Merge branch 'develop' into cds-189
This commit is contained in:
+2
-1
@@ -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,
|
||||
@@ -138,10 +139,11 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
if (StreamChat.of(context).user?.id == message.user?.id)
|
||||
if (StreamChat.of(context).currentUser?.id ==
|
||||
message.user?.id)
|
||||
_buildButton(
|
||||
context,
|
||||
'Delete',
|
||||
context.translations.deleteLabel.capitalize(),
|
||||
StreamSvgIcon.delete(
|
||||
size: 24,
|
||||
color: theme.colorTheme.accentError,
|
||||
|
||||
@@ -141,7 +141,7 @@ class ChannelAvatar extends StatelessWidget {
|
||||
return child;
|
||||
}
|
||||
|
||||
final currentUser = streamChat.user!;
|
||||
final currentUser = streamChat.currentUser!;
|
||||
final otherMembers = channel.state!.members
|
||||
.where((it) => it.userId != currentUser.id)
|
||||
.toList(growable: false);
|
||||
@@ -149,7 +149,7 @@ class ChannelAvatar extends StatelessWidget {
|
||||
// our own space, no other members
|
||||
if (otherMembers.isEmpty) {
|
||||
return BetterStreamBuilder<User>(
|
||||
stream: streamChat.client.state.userStream.map((it) => it!),
|
||||
stream: streamChat.client.state.currentUserStream.map((it) => it!),
|
||||
initialData: currentUser,
|
||||
builder: (context, user) => UserAvatar(
|
||||
borderRadius: borderRadius ?? previewTheme?.borderRadius,
|
||||
|
||||
@@ -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 {
|
||||
@@ -27,8 +28,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
|
||||
final members = channel.state?.members ?? [];
|
||||
|
||||
final userAsMember =
|
||||
members.firstWhere((e) => e.user?.id == _streamChatState.user?.id);
|
||||
final userAsMember = members
|
||||
.firstWhere((e) => e.user?.id == _streamChatState.currentUser?.id);
|
||||
final isOwner = userAsMember.role == 'owner';
|
||||
|
||||
return Material(
|
||||
@@ -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,17 +238,17 @@ 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,
|
||||
),
|
||||
);
|
||||
if (res == true) {
|
||||
final channel = _streamChannelState.channel;
|
||||
final user = _streamChatState.user;
|
||||
final user = _streamChatState.currentUser;
|
||||
if (user != null) {
|
||||
await channel.removeMembers([user.id]);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
@@ -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)
|
||||
@@ -67,7 +71,7 @@ class ChannelInfo extends StatelessWidget {
|
||||
.subtitle,
|
||||
);
|
||||
} else {
|
||||
final userId = StreamChat.of(context).user?.id;
|
||||
final userId = StreamChat.of(context).currentUser?.id;
|
||||
final otherMember = members?.firstWhereOrNull(
|
||||
(element) => element.userId != userId,
|
||||
);
|
||||
@@ -75,12 +79,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,
|
||||
);
|
||||
}
|
||||
@@ -111,7 +116,7 @@ class ChannelInfo extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Searching for Network',
|
||||
context.translations.searchingForNetworkText,
|
||||
style: textStyle,
|
||||
),
|
||||
],
|
||||
@@ -125,7 +130,7 @@ class ChannelInfo extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Offline...',
|
||||
context.translations.offlineLabel,
|
||||
style: textStyle,
|
||||
),
|
||||
TextButton(
|
||||
@@ -141,7 +146,7 @@ class ChannelInfo extends StatelessWidget {
|
||||
..closeConnection()
|
||||
..openConnection(),
|
||||
child: Text(
|
||||
'Try Again',
|
||||
context.translations.tryAgainLabel,
|
||||
style: textStyle?.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.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';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Widget builder for title
|
||||
typedef TitleBuilder = Widget Function(
|
||||
@@ -95,7 +96,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _client = client ?? StreamChat.of(context).client;
|
||||
final user = _client.state.user;
|
||||
final user = _client.state.currentUser;
|
||||
return ConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
var statusString = '';
|
||||
@@ -103,21 +104,20 @@ 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);
|
||||
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,
|
||||
@@ -207,7 +207,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,
|
||||
),
|
||||
@@ -226,7 +226,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Searching for Network',
|
||||
context.translations.searchingForNetworkText,
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelListHeaderTheme
|
||||
.title
|
||||
@@ -247,7 +247,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Offline...',
|
||||
context.translations.offlineLabel,
|
||||
style: chatThemeData.channelListHeaderTheme.title?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -258,7 +258,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
..closeConnection()
|
||||
..openConnection(),
|
||||
child: Text(
|
||||
'Try Again',
|
||||
context.translations.tryAgainLabel,
|
||||
style: chatThemeData.channelListHeaderTheme.title?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:stream_chat_flutter/src/channel_bottom_sheet.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';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Callback called when tapping on a channel
|
||||
typedef ChannelTapCallback = void Function(Channel, Widget?);
|
||||
@@ -301,7 +302,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
'Let’s start chatting!',
|
||||
context.translations.letsStartChattingLabel,
|
||||
style: chatThemeData.textTheme.headline,
|
||||
),
|
||||
),
|
||||
@@ -311,7 +312,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,
|
||||
@@ -330,7 +331,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,
|
||||
),
|
||||
@@ -466,9 +467,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,
|
||||
@@ -476,14 +477,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),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -552,7 +553,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
'owner',
|
||||
].contains(channel.state!.members
|
||||
.firstWhereOrNull(
|
||||
(m) => m.userId == channel.client.state.user?.id)
|
||||
(m) => m.userId == channel.client.state.currentUser?.id)
|
||||
?.role))
|
||||
IconSlideAction(
|
||||
color: backgroundColor,
|
||||
@@ -561,17 +562,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,
|
||||
),
|
||||
@@ -668,7 +668,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,10 +45,12 @@ class ChannelName extends StatelessWidget {
|
||||
) =>
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
var title = 'No title';
|
||||
if (extraData['name'] == null) {
|
||||
final otherMembers =
|
||||
members?.where((member) => member.userId != client.user!.id);
|
||||
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) {
|
||||
if (otherMembers!.first.user != null) {
|
||||
title = otherMembers.first.user!.name;
|
||||
@@ -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(
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:jiffy/jiffy.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';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
@@ -101,7 +102,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
if (members?.isEmpty == true ||
|
||||
members?.any((Member e) =>
|
||||
e.user!.id ==
|
||||
channel.client.state.user?.id) !=
|
||||
channel.client.state.currentUser?.id) !=
|
||||
true) {
|
||||
return const SizedBox();
|
||||
}
|
||||
@@ -124,7 +125,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
(m) => !m.isDeleted && m.shadowed != true,
|
||||
);
|
||||
if (lastMessage?.user?.id ==
|
||||
streamChatState.user?.id) {
|
||||
streamChatState.currentUser?.id) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: SendingIndicator(
|
||||
@@ -133,7 +134,8 @@ class ChannelPreview extends StatelessWidget {
|
||||
isMessageRead: channel.state!.read
|
||||
?.where((element) =>
|
||||
element.user.id !=
|
||||
channel.client.state.user!.id)
|
||||
channel
|
||||
.client.state.currentUser!.id)
|
||||
.where((element) => element.lastRead
|
||||
.isAfter(lastMessage.createdAt))
|
||||
.isNotEmpty ==
|
||||
@@ -172,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 {
|
||||
@@ -197,7 +199,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
size: 16,
|
||||
),
|
||||
Text(
|
||||
' Channel is muted',
|
||||
' ${context.translations.channelIsMutedText}',
|
||||
style: chatThemeData.channelPreviewTheme.subtitle,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Widget to display deleted message
|
||||
class DeletedMessage extends StatelessWidget {
|
||||
@@ -49,7 +50,7 @@ class DeletedMessage extends StatelessWidget {
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Text(
|
||||
'Message deleted',
|
||||
context.translations.messageDeletedLabel,
|
||||
style: messageTheme.messageText?.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: messageTheme.createdAt?.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 {
|
||||
@@ -182,9 +182,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();
|
||||
},
|
||||
@@ -197,7 +198,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.message.type != 'ephemeral')
|
||||
if (!widget.message.isEphemeral)
|
||||
GalleryFooter(
|
||||
currentPage: _currentPage,
|
||||
totalPages: widget.mediaAttachments.length,
|
||||
@@ -222,22 +223,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) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
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 {
|
||||
@@ -135,7 +136,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,
|
||||
),
|
||||
],
|
||||
@@ -191,7 +194,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Photos',
|
||||
context.translations.photosLabel,
|
||||
style:
|
||||
galleryFooterThemeData.bottomSheetPhotosTextStyle,
|
||||
),
|
||||
|
||||
@@ -67,7 +67,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,
|
||||
@@ -78,7 +78,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
),
|
||||
],
|
||||
centerTitle: true,
|
||||
title: message.type != 'ephemeral'
|
||||
title: !message.isEphemeral
|
||||
? InkWell(
|
||||
onTap: onTitleTap,
|
||||
child: SizedBox(
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Fallback user avatar with a polygon gradient overlayed with text
|
||||
class GradientAvatar extends StatefulWidget {
|
||||
/// Constructor for [GradientAvatar]
|
||||
const GradientAvatar({
|
||||
Key? key,
|
||||
required this.name,
|
||||
required this.userId,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Name of user to shorten and display
|
||||
final String name;
|
||||
|
||||
/// ID of user to be used for key
|
||||
final String userId;
|
||||
|
||||
@override
|
||||
_GradientAvatarState createState() => _GradientAvatarState();
|
||||
}
|
||||
|
||||
class _GradientAvatarState extends State<GradientAvatar> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: RepaintBoundary(
|
||||
child: CustomPaint(
|
||||
painter: DemoPainter(
|
||||
widget.userId,
|
||||
getShortenedName(widget.name),
|
||||
DefaultTextStyle.of(context).style.fontFamily ?? 'Roboto',
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
String getShortenedName(String name) {
|
||||
var parts = name.split(' ')..removeWhere((e) => e == '');
|
||||
|
||||
if (parts.length > 2) {
|
||||
parts = parts.take(2).toList();
|
||||
}
|
||||
|
||||
var result = '';
|
||||
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
result = result + parts[i][0].toUpperCase();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// Painter for bg polygon gradient
|
||||
class DemoPainter extends CustomPainter {
|
||||
/// Constructor for [DemoPainter]
|
||||
DemoPainter(
|
||||
this.userId,
|
||||
this.username,
|
||||
this.fontFamily,
|
||||
);
|
||||
|
||||
/// Init grid row count
|
||||
static const int rowCount = 5;
|
||||
|
||||
/// Init grid column count
|
||||
static const int columnCount = 5;
|
||||
|
||||
/// User ID used for key
|
||||
String userId;
|
||||
|
||||
/// User name to display
|
||||
String username;
|
||||
|
||||
/// Font family to use
|
||||
String fontFamily;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rowUnit = size.width / columnCount;
|
||||
final columnUnit = size.height / rowCount;
|
||||
final rand = Random(userId.length);
|
||||
|
||||
final squares = <Offset4>[];
|
||||
final points = <Offset>{};
|
||||
final gradient = colorGradients[rand.nextInt(colorGradients.length)];
|
||||
|
||||
for (var i = 0; i < rowCount; i++) {
|
||||
for (var j = 0; j < columnCount; j++) {
|
||||
final off1 = Offset(rowUnit * j, columnUnit * i);
|
||||
final off2 = Offset(rowUnit * (j + 1), columnUnit * i);
|
||||
final off3 = Offset(rowUnit * (j + 1), columnUnit * (i + 1));
|
||||
final off4 = Offset(rowUnit * j, columnUnit * (i + 1));
|
||||
|
||||
points.addAll([off1, off2, off3, off4]);
|
||||
|
||||
final pointsList = points.toList();
|
||||
|
||||
final p1 = pointsList.indexOf(off1);
|
||||
final p2 = pointsList.indexOf(off2);
|
||||
final p3 = pointsList.indexOf(off3);
|
||||
final p4 = pointsList.indexOf(off4);
|
||||
|
||||
squares.add(
|
||||
Offset4(p1, p2, p3, p4, i, j, rowCount, columnCount, gradient));
|
||||
}
|
||||
}
|
||||
|
||||
final list = transformPoints(points, size);
|
||||
squares.forEach((e) => e.draw(canvas, list));
|
||||
|
||||
final smallerSide = size.width > size.height ? size.width : size.height;
|
||||
|
||||
final textSize = smallerSide / 3;
|
||||
|
||||
final dxShift = (username.length == 2 ? 1.45 : 0.9) * textSize / 2;
|
||||
final dyShift = (username.length == 2 ? 1.0 : 1.65) * textSize / 2;
|
||||
|
||||
final fontSize = username.length == 2 ? textSize : textSize * 1.5;
|
||||
|
||||
TextPainter(
|
||||
text: TextSpan(
|
||||
text: username,
|
||||
style: TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
textDirection: TextDirection.ltr)
|
||||
..layout(maxWidth: size.width)
|
||||
..paint(
|
||||
canvas,
|
||||
Offset(
|
||||
(size.width / 2) - dxShift,
|
||||
(size.height / 2) - dyShift,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
|
||||
/// Transforms initial grid into a polygon grid
|
||||
List<Offset> transformPoints(Set<Offset> points, Size size) {
|
||||
final transformedList = <Offset>[];
|
||||
final orgList = points.toList();
|
||||
final rand = Random(userId.length);
|
||||
|
||||
for (var i = 0; i < points.length; i++) {
|
||||
final orgDx = orgList[i].dx;
|
||||
final orgDy = orgList[i].dy;
|
||||
|
||||
if (orgDx == 0 ||
|
||||
orgDy == 0 ||
|
||||
orgDx == size.width ||
|
||||
orgDy == size.height) {
|
||||
transformedList.add(Offset(orgDx, orgDy));
|
||||
continue;
|
||||
}
|
||||
|
||||
final sign1 = rand.nextInt(2) == 1 ? 1 : -1;
|
||||
final sign2 = rand.nextInt(2) == 1 ? 1 : -1;
|
||||
|
||||
final dx = 0.6 * sign1 * rand.nextInt(size.width ~/ columnCount);
|
||||
final dy = 0.6 * sign2 * rand.nextInt(size.height ~/ rowCount);
|
||||
|
||||
transformedList.add(Offset(orgDx + dx, orgDy + dy));
|
||||
}
|
||||
|
||||
return transformedList;
|
||||
}
|
||||
}
|
||||
|
||||
/// Class for storing and drawing four points of a polygon
|
||||
class Offset4 {
|
||||
/// Constructor for [Offset4]
|
||||
Offset4(
|
||||
this.p1,
|
||||
this.p2,
|
||||
this.p3,
|
||||
this.p4,
|
||||
this.row,
|
||||
this.column,
|
||||
this.rowSize,
|
||||
this.colSize,
|
||||
this.gradient,
|
||||
);
|
||||
|
||||
/// Point 1
|
||||
int p1;
|
||||
|
||||
/// Point 2
|
||||
int p2;
|
||||
|
||||
/// Point 3
|
||||
int p3;
|
||||
|
||||
/// Point 4
|
||||
int p4;
|
||||
|
||||
/// Position of polygon on grid
|
||||
int row;
|
||||
|
||||
/// Position of polygon on grid
|
||||
int column;
|
||||
|
||||
/// Max row size
|
||||
int rowSize;
|
||||
|
||||
/// Max col size
|
||||
int colSize;
|
||||
|
||||
/// Gradient to be applied to polygon
|
||||
List<Color> gradient;
|
||||
|
||||
/// Draw the polygon on canvas
|
||||
void draw(Canvas canvas, List<Offset> points) {
|
||||
final paint = Paint()
|
||||
..color = Color.fromARGB(255, Random().nextInt(255),
|
||||
Random().nextInt(255), Random().nextInt(255))
|
||||
..shader = ui.Gradient.linear(
|
||||
points[p1],
|
||||
points[p3],
|
||||
gradient,
|
||||
);
|
||||
|
||||
final backgroundPath = Path()
|
||||
..moveTo(points[p1].dx, points[p1].dy)
|
||||
..lineTo(points[p2].dx, points[p2].dy)
|
||||
..lineTo(points[p3].dx, points[p3].dy)
|
||||
..lineTo(points[p4].dx, points[p4].dy)
|
||||
..lineTo(points[p1].dx, points[p1].dy)
|
||||
..close();
|
||||
|
||||
canvas.drawPath(backgroundPath, paint);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gradient list for polygons
|
||||
const colorGradients = [
|
||||
[Color(0xffffafbd), Color(0xffffc3a0)],
|
||||
[Color(0xff2193b0), Color(0xff6dd5ed)],
|
||||
[Color(0xffcc2b5e), Color(0xff753a88)],
|
||||
[Color(0xffee9ca7), Color(0xffffdde1)],
|
||||
[Color(0xff42275a), Color(0xff734b6d)],
|
||||
[Color(0xffde6262), Color(0xffffb88c)],
|
||||
[Color(0xff56ab2f), Color(0xffa8e063)],
|
||||
[Color(0xff614385), Color(0xff516395)],
|
||||
[Color(0xffeacda3), Color(0xffd6ae7b)],
|
||||
[Color(0xff02aab0), Color(0xff00cdac)],
|
||||
];
|
||||
@@ -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 => 'Let’s 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';
|
||||
}
|
||||
@@ -101,7 +101,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
Widget _showMessageOptionsModal() {
|
||||
final mediaQueryData = MediaQuery.of(context);
|
||||
final size = mediaQueryData.size;
|
||||
final user = StreamChat.of(context).user;
|
||||
final user = StreamChat.of(context).currentUser;
|
||||
|
||||
final roughMaxSize = 2 * size.width / 3;
|
||||
var messageTextLength = widget.message.text!.length;
|
||||
@@ -268,16 +268,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;
|
||||
@@ -290,9 +288,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 &&
|
||||
@@ -303,9 +301,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();
|
||||
@@ -335,14 +333,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) {
|
||||
@@ -366,9 +364,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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -390,7 +388,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
'Reply',
|
||||
context.translations.replyLabel,
|
||||
style: streamChatThemeData.textTheme.body,
|
||||
),
|
||||
],
|
||||
@@ -412,7 +410,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
'Flag Message',
|
||||
context.translations.flagMessageLabel,
|
||||
style: streamChatThemeData.textTheme.body,
|
||||
),
|
||||
],
|
||||
@@ -435,7 +433,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,
|
||||
),
|
||||
],
|
||||
@@ -458,7 +458,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
|
||||
@@ -487,7 +489,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
'Copy Message',
|
||||
context.translations.copyMessageLabel,
|
||||
style: streamChatThemeData.textTheme.body,
|
||||
),
|
||||
],
|
||||
@@ -512,7 +514,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
'Edit Message',
|
||||
context.translations.editMessageLabel,
|
||||
style: streamChatThemeData.textTheme.body,
|
||||
),
|
||||
],
|
||||
@@ -544,7 +546,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,
|
||||
),
|
||||
],
|
||||
@@ -588,9 +592,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,
|
||||
@@ -636,7 +640,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
'Thread Reply',
|
||||
context.translations.threadReplyLabel,
|
||||
style: streamChatThemeData.textTheme.body,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -380,9 +380,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,
|
||||
@@ -484,7 +484,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),
|
||||
@@ -640,7 +640,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
style: _streamChatTheme.messageInputTheme.inputTextStyle,
|
||||
autofocus: widget.autofocus,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: _getInputDecoration(),
|
||||
decoration: _getInputDecoration(context),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
)
|
||||
@@ -652,11 +652,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration _getInputDecoration() {
|
||||
InputDecoration _getInputDecoration(BuildContext context) {
|
||||
final passedDecoration = _streamChatTheme.messageInputTheme.inputDecoration;
|
||||
return InputDecoration(
|
||||
isDense: true,
|
||||
hintText: _getHint(),
|
||||
hintText: _getHint(context),
|
||||
hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith(
|
||||
color: _streamChatTheme.colorTheme.textLowEmphasis,
|
||||
),
|
||||
@@ -805,14 +805,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) {
|
||||
@@ -935,7 +935,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Instant Commands',
|
||||
context.translations.instantCommandsLabel,
|
||||
style: TextStyle(
|
||||
color: _streamChatTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(.5),
|
||||
@@ -1198,8 +1198,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;
|
||||
}
|
||||
@@ -1210,9 +1211,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;
|
||||
}
|
||||
}
|
||||
@@ -1480,7 +1481,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Emoji matching "$query"',
|
||||
context.translations.emojiMatchingQueryText(
|
||||
query,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: _streamChatTheme
|
||||
.colorTheme.textHighEmphasis
|
||||
@@ -1830,17 +1833,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);
|
||||
@@ -1848,7 +1851,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);
|
||||
@@ -1857,7 +1860,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);
|
||||
@@ -1866,7 +1869,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);
|
||||
@@ -1874,7 +1877,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);
|
||||
@@ -1977,8 +1980,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;
|
||||
}
|
||||
@@ -1989,9 +1993,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;
|
||||
}
|
||||
}
|
||||
@@ -2172,7 +2176,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
height: 26,
|
||||
),
|
||||
Text(
|
||||
'Something went wrong',
|
||||
context.translations.somethingWentWrongError,
|
||||
style: _streamChatTheme.textTheme.headlineBold,
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -2201,7 +2205,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),
|
||||
),
|
||||
@@ -2312,10 +2316,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
|
||||
@@ -2347,7 +2351,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,
|
||||
@@ -2379,8 +2383,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),
|
||||
@@ -2389,7 +2392,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,
|
||||
),
|
||||
|
||||
@@ -166,11 +166,23 @@ class MessageListView extends StatefulWidget {
|
||||
this.showFloatingDateDivider = true,
|
||||
this.threadSeparatorBuilder,
|
||||
this.messageListController,
|
||||
this.reverse = true,
|
||||
this.paginationLimit = 20,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Function used to build a custom message widget
|
||||
final MessageBuilder? messageBuilder;
|
||||
|
||||
/// Whether the view scrolls in the reading direction.
|
||||
///
|
||||
/// Defaults to true.
|
||||
///
|
||||
/// See [ScrollView.reverse].
|
||||
final bool reverse;
|
||||
|
||||
/// Limit used during pagination
|
||||
final int paginationLimit;
|
||||
|
||||
/// Function used to build a custom system message widget
|
||||
final SystemMessageBuilder? systemMessageBuilder;
|
||||
|
||||
@@ -323,11 +335,13 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
bool _inBetweenList = false;
|
||||
|
||||
late final _defaultController = MessageListController();
|
||||
|
||||
MessageListController get _messageListController =>
|
||||
widget.messageListController ?? _defaultController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MessageListCore(
|
||||
paginationLimit: widget.paginationLimit,
|
||||
messageFilter: widget.messageFilter,
|
||||
loadingBuilder: widget.loadingBuilder ??
|
||||
(context) => const Center(
|
||||
@@ -336,7 +350,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)),
|
||||
@@ -349,7 +363,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)),
|
||||
@@ -395,14 +409,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;
|
||||
}
|
||||
|
||||
@@ -445,7 +459,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
initialAlignment: initialAlignment ?? 0,
|
||||
physics: widget.scrollPhysics,
|
||||
itemScrollController: _scrollController,
|
||||
reverse: true,
|
||||
reverse: widget.reverse,
|
||||
addAutomaticKeepAlives: false,
|
||||
itemCount: itemCount,
|
||||
|
||||
@@ -604,7 +618,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,
|
||||
@@ -612,7 +626,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.channelTheme.channelHeaderTheme.subtitle,
|
||||
),
|
||||
@@ -621,7 +635,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
|
||||
Positioned _buildFloatingDateDivider(int itemCount) => Positioned(
|
||||
top: 20,
|
||||
top: widget.reverse ? 20 : null,
|
||||
bottom: widget.reverse ? null : 20,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: BetterStreamBuilder<Iterable<ItemPosition>>(
|
||||
@@ -655,7 +670,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
);
|
||||
|
||||
Future<void> _paginateData(
|
||||
StreamChannelState? channel, QueryDirection direction) =>
|
||||
StreamChannelState? channel,
|
||||
QueryDirection direction,
|
||||
) =>
|
||||
_messageListController.paginateData!(direction: direction);
|
||||
|
||||
int? _getTopElementIndex(Iterable<ItemPosition> values) {
|
||||
@@ -687,7 +704,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final unreadCount = snapshot.data!.item2;
|
||||
final showUnreadCount = unreadCount > 0 &&
|
||||
streamChannel!.channel.state!.members.any((e) =>
|
||||
e.userId == streamChannel!.channel.client.state.user!.id);
|
||||
e.userId ==
|
||||
streamChannel!.channel.client.state.currentUser!.id);
|
||||
return Positioned(
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
@@ -715,9 +733,13 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
);
|
||||
}
|
||||
},
|
||||
child: StreamSvgIcon.down(
|
||||
color: _streamTheme.colorTheme.textHighEmphasis,
|
||||
),
|
||||
child: widget.reverse
|
||||
? StreamSvgIcon.down(
|
||||
color: _streamTheme.colorTheme.textHighEmphasis,
|
||||
)
|
||||
: StreamSvgIcon.up(
|
||||
color: _streamTheme.colorTheme.textHighEmphasis,
|
||||
),
|
||||
),
|
||||
if (showUnreadCount)
|
||||
Positioned(
|
||||
@@ -789,9 +811,10 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
Widget buildParentMessage(
|
||||
Message message,
|
||||
) {
|
||||
final isMyMessage = message.user!.id == StreamChat.of(context).user!.id;
|
||||
final isMyMessage =
|
||||
message.user!.id == StreamChat.of(context).currentUser!.id;
|
||||
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
||||
final currentUser = StreamChat.of(context).user;
|
||||
final currentUser = StreamChat.of(context).currentUser;
|
||||
final members = StreamChannel.of(context).channel.state?.members ?? [];
|
||||
final currentUserMember =
|
||||
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
||||
@@ -876,7 +899,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
);
|
||||
}
|
||||
|
||||
final userId = StreamChat.of(context).user!.id;
|
||||
final userId = StreamChat.of(context).currentUser!.id;
|
||||
final isMyMessage = message.user!.id == userId;
|
||||
final nextMessage = index - 1 >= 0 ? messages[index - 1] : null;
|
||||
final isNextUserSame =
|
||||
@@ -939,7 +962,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
? BorderSide.none
|
||||
: null;
|
||||
|
||||
final currentUser = StreamChat.of(context).user;
|
||||
final currentUser = StreamChat.of(context).currentUser;
|
||||
final members = StreamChannel.of(context).channel.state?.members ?? [];
|
||||
final currentUserMember =
|
||||
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
||||
@@ -1145,7 +1168,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
_topPaginationActive = false;
|
||||
}
|
||||
if (event.message!.user!.id ==
|
||||
streamChannel!.channel.client.state.user!.id) {
|
||||
streamChannel!.channel.client.state.currentUser!.id) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
_scrollController?.jumpTo(
|
||||
index: 0,
|
||||
@@ -1227,8 +1250,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) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:stream_chat_flutter/src/stream_chat.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 {
|
||||
@@ -42,7 +43,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final user = StreamChat.of(context).user;
|
||||
final user = StreamChat.of(context).currentUser;
|
||||
|
||||
final roughMaxSize = 2 * size.width / 3;
|
||||
var messageTextLength = message.text!.length;
|
||||
@@ -154,7 +155,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.
|
||||
///
|
||||
@@ -49,12 +50,14 @@ class MessageSearchItem extends StatelessWidget {
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
user.id == StreamChat.of(context).user?.id ? 'You' : user.name,
|
||||
user.id == StreamChat.of(context).currentUser?.id
|
||||
? context.translations.youText
|
||||
: user.name,
|
||||
style: chatThemeData.channelPreviewTheme.title,
|
||||
),
|
||||
if (channelName != null) ...[
|
||||
Text(
|
||||
' in ',
|
||||
' ${context.translations.inText} ',
|
||||
style: chatThemeData.channelPreviewTheme.title?.copyWith(
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
@@ -98,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) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:stream_chat_flutter/src/info_tile.dart';
|
||||
import 'package:stream_chat_flutter/src/message_search_item.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);
|
||||
@@ -140,6 +141,7 @@ class MessageSearchListView extends StatefulWidget {
|
||||
|
||||
class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
late final _defaultController = MessageSearchListController();
|
||||
|
||||
MessageSearchListController get _messageSearchListController =>
|
||||
widget.messageSearchListController ?? _defaultController;
|
||||
|
||||
@@ -161,8 +163,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),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -176,7 +178,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(),
|
||||
);
|
||||
},
|
||||
@@ -240,10 +242,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),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -306,7 +308,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Text(
|
||||
'${items.length} results',
|
||||
context.translations.resultCountText(items.length),
|
||||
style: TextStyle(
|
||||
color: chatThemeData.colorTheme.textLowEmphasis,
|
||||
),
|
||||
|
||||
@@ -29,60 +29,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.messageText?.color,
|
||||
decoration: messageTheme.messageText?.decoration,
|
||||
decorationColor: messageTheme.messageText?.decorationColor,
|
||||
decorationStyle: messageTheme.messageText?.decorationStyle,
|
||||
fontFamily: messageTheme.messageText?.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.messageText?.color,
|
||||
decoration: messageTheme.messageText?.decoration,
|
||||
decorationColor: messageTheme.messageText?.decorationColor,
|
||||
decorationStyle: messageTheme.messageText?.decorationStyle,
|
||||
fontFamily: messageTheme.messageText?.fontFamily,
|
||||
),
|
||||
),
|
||||
).copyWith(
|
||||
a: messageTheme.messageLinks,
|
||||
p: messageTheme.messageText,
|
||||
),
|
||||
),
|
||||
).copyWith(
|
||||
a: messageTheme.messageLinks,
|
||||
p: messageTheme.messageText,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,8 @@ class MessageWidget extends StatefulWidget {
|
||||
this.userAvatarBuilder,
|
||||
this.editMessageInputBuilder,
|
||||
this.textBuilder,
|
||||
this.bottomRowBuilder,
|
||||
this.deletedBottomRowBuilder,
|
||||
this.onReturnAction,
|
||||
Map<String, AttachmentBuilder>? customAttachmentBuilders,
|
||||
this.readList,
|
||||
@@ -275,6 +277,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;
|
||||
|
||||
@@ -410,6 +418,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,
|
||||
MessageTheme? messageTheme,
|
||||
@@ -463,6 +473,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,
|
||||
@@ -782,7 +795,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(
|
||||
@@ -810,7 +827,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
}
|
||||
|
||||
Widget _buildQuotedMessage() {
|
||||
final isMyMessage = widget.message.user?.id == _streamChat.user?.id;
|
||||
final isMyMessage = widget.message.user?.id == _streamChat.currentUser?.id;
|
||||
final onTap = widget.message.quotedMessage?.isDeleted != true &&
|
||||
widget.onQuotedMessageTap != null
|
||||
? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId)
|
||||
@@ -830,22 +847,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>[];
|
||||
@@ -854,9 +860,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
|
||||
@@ -993,7 +999,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
Widget _buildReactionIndicator(
|
||||
BuildContext context,
|
||||
) {
|
||||
final ownId = _streamChat.user!.id;
|
||||
final ownId = _streamChat.currentUser!.id;
|
||||
final reactionsMap = <String, Reaction>{};
|
||||
widget.message.latestReactions?.forEach((element) {
|
||||
if (!reactionsMap.containsKey(element.type) ||
|
||||
@@ -1054,10 +1060,10 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
showReactionPickerIndicator: widget.showReactions &&
|
||||
(widget.message.status == MessageSendingStatus.sent),
|
||||
showPinHighlight: false,
|
||||
showUserAvatar:
|
||||
widget.message.user!.id == channel.client.state.user!.id
|
||||
? DisplayWidget.gone
|
||||
: DisplayWidget.show,
|
||||
showUserAvatar: widget.message.user!.id ==
|
||||
channel.client.state.currentUser!.id
|
||||
? DisplayWidget.gone
|
||||
: DisplayWidget.show,
|
||||
),
|
||||
onCopyTap: (message) =>
|
||||
Clipboard.setData(ClipboardData(text: message.text)),
|
||||
@@ -1118,7 +1124,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
(widget.message.status == MessageSendingStatus.sent),
|
||||
showPinHighlight: false,
|
||||
showUserAvatar:
|
||||
widget.message.user!.id == channel.client.state.user!.id
|
||||
widget.message.user!.id == channel.client.state.currentUser!.id
|
||||
? DisplayWidget.gone
|
||||
: DisplayWidget.show,
|
||||
),
|
||||
@@ -1201,7 +1207,10 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
'Uploading $uploadRemaining/$totalAttachments ...',
|
||||
context.translations.attachmentsUploadProgressText(
|
||||
remaining: uploadRemaining,
|
||||
total: totalAttachments,
|
||||
),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
@@ -1275,8 +1284,8 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
}
|
||||
|
||||
Widget _buildPinnedMessage(Message message) {
|
||||
final pinnedBy = message.pinnedBy;
|
||||
final pinnedByMe = _streamChat.user!.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),
|
||||
@@ -1290,7 +1299,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,
|
||||
|
||||
@@ -123,7 +123,7 @@ class ReactionBubble extends StatelessWidget {
|
||||
);
|
||||
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
final userId = StreamChat.of(context).user?.id;
|
||||
final userId = StreamChat.of(context).currentUser?.id;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -126,21 +125,34 @@ class StreamChatState extends State<StreamChat> {
|
||||
return defaultTheme.merge(themeData);
|
||||
}
|
||||
|
||||
// coverage:ignore-start
|
||||
|
||||
/// The current user
|
||||
User? get user => widget.client.state.user;
|
||||
@Deprecated('Use `.currentUser` instead, Will be removed in future releases')
|
||||
User? get user => widget.client.state.currentUser;
|
||||
|
||||
/// The current user as a stream
|
||||
Stream<User?> get userStream => widget.client.state.userStream;
|
||||
@Deprecated(
|
||||
'Use `.currentUserStream` instead, Will be removed in future releases',
|
||||
)
|
||||
Stream<User?> get userStream => widget.client.state.currentUserStream;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
// coverage:ignore-end
|
||||
|
||||
/// The current user
|
||||
User? get currentUser => widget.client.state.currentUser;
|
||||
|
||||
/// The current user as a stream
|
||||
Stream<User?> get currentUserStream => widget.client.state.currentUserStream;
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_header.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_preview.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/gradient_avatar.dart';
|
||||
import 'package:stream_chat_flutter/src/message_input.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
@@ -310,10 +309,9 @@ class StreamChatThemeData {
|
||||
colorTheme: colorTheme,
|
||||
primaryIconTheme: iconTheme,
|
||||
defaultUserImage: (context, user) => Center(
|
||||
child: CachedNetworkImage(
|
||||
filterQuality: FilterQuality.high,
|
||||
imageUrl: getRandomPicUrl(user),
|
||||
fit: BoxFit.cover,
|
||||
child: GradientAvatar(
|
||||
name: user.name,
|
||||
userId: user.id,
|
||||
),
|
||||
),
|
||||
channelPreviewTheme: channelPreviewTheme,
|
||||
|
||||
@@ -37,6 +37,18 @@ class StreamSvgIcon extends StatelessWidget {
|
||||
height: size,
|
||||
);
|
||||
|
||||
/// [StreamSvgIcon] type
|
||||
factory StreamSvgIcon.up({
|
||||
double? size,
|
||||
Color? color,
|
||||
}) =>
|
||||
StreamSvgIcon(
|
||||
assetName: 'Icon_up.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
|
||||
/// [StreamSvgIcon] type
|
||||
factory StreamSvgIcon.attach({
|
||||
double? size,
|
||||
|
||||
@@ -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';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
@@ -111,7 +112,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'with ',
|
||||
'${context.translations.withText} ',
|
||||
style: chatThemeData.channelTheme.channelHeaderTheme.subtitle,
|
||||
),
|
||||
Flexible(
|
||||
@@ -149,7 +150,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
children: [
|
||||
title ??
|
||||
Text(
|
||||
'Thread Reply',
|
||||
context.translations.threadReplyLabel,
|
||||
style: chatThemeData.channelTheme.channelHeaderTheme.title,
|
||||
),
|
||||
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)),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Callback called when tapping on a user
|
||||
typedef UserTapCallback = void Function(User, Widget?);
|
||||
@@ -155,6 +156,7 @@ class _UserListViewState extends State<UserListView>
|
||||
bool get _isListView => widget.crossAxisCount == 1;
|
||||
|
||||
late final _defaultController = UserListController();
|
||||
|
||||
UserListController get _userListController =>
|
||||
widget.userListController ?? _defaultController;
|
||||
|
||||
@@ -220,9 +222,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,
|
||||
@@ -230,14 +232,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),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -250,8 +252,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),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -400,10 +402,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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user