Merge remote-tracking branch 'origin/develop' into feat/localization
# Conflicts: # packages/stream_chat_flutter/lib/src/message_text.dart
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
✅ Added
|
||||
|
||||
- Added `MessageListView.paginationLimit`
|
||||
- `MessageText` renders message translation if available
|
||||
- Allow the various ListView widgets to be themed via ThemeData classes
|
||||
- Added `bottomRowBuilder` and `deletedBottomRowBuilder` that build a widget below a `MessageWidget`
|
||||
|
||||
|
||||
@@ -29,55 +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;
|
||||
|
||||
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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,33 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'mocks.dart';
|
||||
import 'simple_frame.dart';
|
||||
|
||||
void expectTextStrings(Iterable<Widget> widgets, List<String> strings) {
|
||||
var currentString = 0;
|
||||
for (final widget in widgets) {
|
||||
if (widget is RichText) {
|
||||
final span = widget.text as TextSpan;
|
||||
final text = _extractTextFromTextSpan(span);
|
||||
expect(text, equals(strings[currentString]));
|
||||
currentString += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _extractTextFromTextSpan(TextSpan span) {
|
||||
var text = span.text ?? '';
|
||||
if (span.children != null) {
|
||||
for (final child in span.children! as Iterable<TextSpan>) {
|
||||
text += _extractTextFromTextSpan(child);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'it should show correct message text',
|
||||
(WidgetTester tester) async {
|
||||
final currentUser = OwnUser(id: 'user-id');
|
||||
final client = MockClient();
|
||||
final clientState = MockClientState();
|
||||
final channel = MockChannel();
|
||||
@@ -21,7 +44,9 @@ void main() {
|
||||
final streamTheme = StreamChatThemeData.fromTheme(themeData);
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(currentUser);
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(currentUser));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
@@ -54,9 +79,107 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
group('Message with i18n field', () {
|
||||
final client = MockClient();
|
||||
final clientState = MockClientState();
|
||||
final channel = MockChannel();
|
||||
final channelState = MockChannelState();
|
||||
const messageTheme = MessageTheme();
|
||||
|
||||
final currentUser = OwnUser(
|
||||
id: 'sahil',
|
||||
language: 'hi',
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.currentUser).thenReturn(currentUser);
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(currentUser));
|
||||
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.isMuted).thenReturn(false);
|
||||
when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false));
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'should show correct translated message text as per user language',
|
||||
(WidgetTester tester) async {
|
||||
final message = Message(
|
||||
text: 'Hello',
|
||||
i18n: const {
|
||||
'en_text': 'Hello',
|
||||
'hi_text': 'नमस्ते',
|
||||
'language': 'en',
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StreamChat(
|
||||
client: client,
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: Scaffold(
|
||||
body: MessageText(
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(MarkdownBody), findsOneWidget);
|
||||
|
||||
final widgets = tester.allWidgets;
|
||||
expectTextStrings(widgets, <String>['नमस्ते']);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'''should show default text if i18n does not contain translations as per user language''',
|
||||
(WidgetTester tester) async {
|
||||
final message = Message(
|
||||
text: 'Hello',
|
||||
i18n: const {
|
||||
'en_text': 'Hello',
|
||||
'fr_text': 'Bonjour',
|
||||
'language': 'en',
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StreamChat(
|
||||
client: client,
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: Scaffold(
|
||||
body: MessageText(
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(MarkdownBody), findsOneWidget);
|
||||
|
||||
final widgets = tester.allWidgets;
|
||||
expectTextStrings(widgets, <String>['Hello']);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testGoldens(
|
||||
'control test',
|
||||
(WidgetTester tester) async {
|
||||
final currentUser = OwnUser(id: 'user-id');
|
||||
final client = MockClient();
|
||||
final clientState = MockClientState();
|
||||
final channel = MockChannel();
|
||||
@@ -66,7 +189,9 @@ void main() {
|
||||
final streamTheme = StreamChatThemeData.fromTheme(themeData);
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(currentUser);
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(currentUser));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
@@ -90,14 +215,18 @@ cool.''';
|
||||
|
||||
await tester.pumpWidgetBuilder(
|
||||
materialAppWrapper()(SimpleFrame(
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: Scaffold(
|
||||
body: MessageText(
|
||||
message: Message(
|
||||
text: messageText,
|
||||
child: StreamChat(
|
||||
client: client,
|
||||
connectivityStream: Stream.value(ConnectivityResult.wifi),
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: Scaffold(
|
||||
body: MessageText(
|
||||
message: Message(
|
||||
text: messageText,
|
||||
),
|
||||
messageTheme: streamTheme.otherMessageTheme,
|
||||
),
|
||||
messageTheme: streamTheme.otherMessageTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:golden_toolkit/golden_toolkit.dart';
|
||||
import 'package:golden_toolkit/src/testing_tools.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
const double _kGoldenDiffTolerance = 0.05;
|
||||
|
||||
/// Wrapper function for golden tests.
|
||||
Future<void> customExpectGoldenMatches(
|
||||
WidgetTester tester,
|
||||
String name, {
|
||||
bool? autoHeight,
|
||||
Finder? finder,
|
||||
CustomPump? customPump,
|
||||
@Deprecated('''
|
||||
This method level parameter will be removed in an upcoming release. This can be configured globally. If you have concerns, please file an issue with your use case.''') bool? skip,
|
||||
}) {
|
||||
final goldenPath = path.join('test/src/goldens');
|
||||
print('goldenPath: $goldenPath');
|
||||
goldenFileComparator = CustomGoldenFileComparator(Uri.parse(goldenPath));
|
||||
|
||||
return compareWithGolden(
|
||||
tester,
|
||||
name,
|
||||
autoHeight: autoHeight,
|
||||
finder: finder,
|
||||
customPump: customPump,
|
||||
skip: skip,
|
||||
// This value is actually ignored. We are forced to pass it because the
|
||||
// downstream API is structured poorly. This should be refactored.
|
||||
device: Device.phone,
|
||||
fileNameFactory: (String name, Device device) =>
|
||||
GoldenToolkit.configuration.fileNameFactory(name),
|
||||
);
|
||||
}
|
||||
|
||||
class CustomGoldenFileComparator extends LocalFileComparator {
|
||||
CustomGoldenFileComparator(Uri testFile) : super(testFile);
|
||||
|
||||
@override
|
||||
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
|
||||
print('golden.toString(): ${golden.toString()}');
|
||||
final result = await GoldenFileComparator.compareLists(
|
||||
imageBytes,
|
||||
await getGoldenBytes(golden),
|
||||
);
|
||||
|
||||
if (!result.passed && result.diffPercent > _kGoldenDiffTolerance) {
|
||||
final error = await generateFailureOutput(result, golden, basedir);
|
||||
throw FlutterError(error);
|
||||
}
|
||||
return result.passed;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user