Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
- Added `onReactionTap` to `StreamMessageItem` and `StreamMessageListView`, reporting the tapped message's `BuildContext` and a `ReactionTapDetails` with the tapped `message` and `reaction` (the reaction is `null` for a clustered or overflow chip that maps to no single reaction).
- Exported `StreamEphemeralMessage`, the row `StreamMessageListView` builds for ephemeral messages, matching its already-exported `StreamSystemMessage` and `StreamModeratedMessage` siblings.
- Added an `unreadIndicator` parameter to `StreamBackButton` that overlays a widget (typically a `StreamUnreadIndicator`) on the button's top-end corner. Pass `StreamUnreadIndicator(excludeCid: cid)` to show the total unread count of other channels, or `StreamUnreadIndicator.channels(cid: cid)` for a single channel's count.
- Exported this package's `StreamMessageContent`, which was previously unreachable from `package:stream_chat_flutter/stream_chat_flutter.dart`.
- Added `StreamChatConfigurationData.messageTranslation`, a `StreamMessageTranslationConfiguration` with `enabled` (default `true`) to display translations from `Message.i18n` and `annotationEnabled` (default `false`) to opt into a "Translated"/"Original" annotation with a toggle link.
- Added `StreamMessageTranslationStore`, tracking the annotation's toggle state per `StreamMessageListView`. Also drivable via `showTranslatedText` on `StreamMessageHeaderProps`, `StreamMessageContent`, and `StreamMessageText`, and `onToggleTranslatedText` on `StreamMessageHeaderProps`.

⚠️ Deprecated

Expand All @@ -50,6 +53,7 @@

🐞 Fixed

- Fixed message text and previews translating to English for users with no `User.language` set; they now show the original text.
- Fixed a failed send surfacing as an unhandled async error when `StreamMessageComposer` has no `onError`; it is now reported through `FlutterError.reportError`.
- Fixed the default `StreamChannel` loading and error states not being themed or localized; `StreamChat` now installs themed, connection-aware defaults, overridable per `StreamChannel` or via `DefaultStreamChannelBuilders`.
- Fixed the default list/scroll-view error states (channel, message, member, user, thread, poll-vote, reaction, search, and photo) showing raw or fixed errors; they are now connection-aware (no internet / slow connection), falling back to each view's specific error text.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

/// A widget that renders a preview of the message text.
///
/// The preview is translated into the current user's language when
/// [Message.i18n] has one, unless disabled SDK-wide via
/// [StreamMessageTranslationConfiguration.enabled] — matching the same
/// opt-out [StreamMessageText] respects for the full message bubble.
class StreamMessagePreviewText extends StatelessWidget {
/// Creates a new instance of [StreamMessagePreviewText].
const StreamMessagePreviewText({
Expand All @@ -20,6 +25,8 @@ class StreamMessagePreviewText extends StatelessWidget {
final ChannelModel? channel;

/// The language to use for translations.
///
/// Defaults to the current user's [User.language].
final String? language;

/// The style to use for the text.
Expand All @@ -34,11 +41,12 @@ class StreamMessagePreviewText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final currentUser = StreamChat.maybeOf(context)?.currentUser;
final translationLanguage = language ?? currentUser?.language ?? 'en';
final translatedMessage = message.translate(translationLanguage);
final config = StreamChatConfiguration.of(context);
final translationConfig = config.messageTranslation;
final translationLanguage = language ?? currentUser?.language;
final translatedMessage = translationConfig.enabled ? message.translate(translationLanguage) : message;
final previewMessage = translatedMessage.replaceMentions(linkify: false);

final config = StreamChatConfiguration.of(context);
final formatter = config.messagePreviewFormatter;

final previewTextSpan = formatter.formatMessage(
Expand Down
107 changes: 107 additions & 0 deletions packages/stream_chat_flutter/lib/src/localization/translations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,32 @@ abstract class Translations {
/// The text displaying the reminder time (e.g. "Today at 3:00 PM").
String reminderAtText(String time);

/// The annotation label shown on a message currently displayed as a
/// translation (e.g. "Translated").
String get translatedLabel;

/// The annotation label shown on a message currently displayed in its
/// original language, despite a translation being available (e.g.
/// "Original").
String get originalLabel;

/// The link label that reverts a translated message to its original text.
String get showOriginalLabel;

/// The link label that reveals a translation for a message currently shown
/// in its original language.
String get showTranslationLabel;

/// The annotation label shown on a message currently displayed as a
/// translation, when the message's original language is known.
///
/// [languageCode] is the raw source-language code Stream Chat's
/// translation API reports via `Message.i18n['language']` — an ISO 639-1
/// code, optionally with a regional suffix (e.g. `es-MX`). Implementations
/// resolve it to a localized display name, producing something like
/// "Translated from Spanish".
String translatedFromLanguageText(String languageCode);

/// The label for "Create a poll and let everyone vote!"
String get createPollPromptLabel;

Expand Down Expand Up @@ -1601,6 +1627,21 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments
@override
String reminderAtText(String time) => 'Today at $time';

@override
String get translatedLabel => 'Translated';

@override
String get originalLabel => 'Original';

@override
String get showOriginalLabel => 'Show original';

@override
String get showTranslationLabel => 'Show translation';

@override
String translatedFromLanguageText(String languageCode) => 'Translated from ${_languageName(languageCode)}';

@override
String get createPollPromptLabel => 'Create a poll and let everyone vote!';

Expand Down Expand Up @@ -1646,3 +1687,69 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments
@override
String notifyRoleText(String role) => 'Notify all $role members';
}

// Display name for each language code Stream Chat's translation API
// supports (`Message.i18n['language']`), for use in
// `DefaultTranslations.translatedFromLanguageText`. Falls back to the
// uppercased code itself for anything unrecognized.
String _languageName(String code) => _languageNames[code] ?? code.toUpperCase();

const _languageNames = <String, String>{
'af': 'Afrikaans',
'sq': 'Albanian',
'am': 'Amharic',
'ar': 'Arabic',
'az': 'Azerbaijani',
'bn': 'Bengali',
'bs': 'Bosnian',
'bg': 'Bulgarian',
'zh': 'Chinese (Simplified)',
'zh-TW': 'Chinese (Traditional)',
'hr': 'Croatian',
'cs': 'Czech',
'da': 'Danish',
'fa-AF': 'Dari',
'nl': 'Dutch',
'en': 'English',
'et': 'Estonian',
'fi': 'Finnish',
'fr': 'French',
'fr-CA': 'French (Canada)',
'ka': 'Georgian',
'de': 'German',
'el': 'Greek',
'ht': 'Haitian Creole',
'ha': 'Hausa',
'he': 'Hebrew',
'hi': 'Hindi',
'hu': 'Hungarian',
'id': 'Indonesian',
'it': 'Italian',
'ja': 'Japanese',
'ko': 'Korean',
'lv': 'Latvian',
'lt': 'Lithuanian',
'ms': 'Malay',
'no': 'Norwegian',
'fa': 'Persian',
'ps': 'Pashto',
'pl': 'Polish',
'pt': 'Portuguese',
'ro': 'Romanian',
'ru': 'Russian',
'sr': 'Serbian',
'sk': 'Slovak',
'sl': 'Slovenian',
'so': 'Somali',
'es': 'Spanish',
'es-MX': 'Spanish (Mexico)',
'sw': 'Swahili',
'sv': 'Swedish',
'tl': 'Tagalog',
'ta': 'Tamil',
'th': 'Thai',
'tr': 'Turkish',
'uk': 'Ukrainian',
'ur': 'Urdu',
'vi': 'Vietnamese',
};
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,11 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
late final ItemPositionsListener _itemPositionListener;
StreamChannelState? streamChannel;

// Owned per list instance, so a channel view and an open thread (a
// separate `StreamMessageListView`) each get independent translation
// toggle state. See [StreamMessageTranslationStore].
final _translationStore = StreamMessageTranslationStore();

// Drives the unread-messages separator. Held in a [ValueNotifier] so read
// events can update it without rebuilding the entire list view.
final _unreadState = ValueNotifier<({int count, String? firstUnreadId})>((count: 0, firstUnreadId: null));
Expand Down Expand Up @@ -479,6 +484,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
debouncedMarkThreadRead.cancel();
_unreadState.dispose();
_highlightState.dispose();
_translationStore.dispose();
super.dispose();
}

Expand Down Expand Up @@ -616,21 +622,24 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
// TODO: Revisit this nested Portal setup during desktop reactions refactor
// and remove the extra layer if a dedicated message-list portal label is
// no longer required.
return Portal(
labels: const [kPortalMessageListViewLabel],
return StreamMessageTranslations(
store: _translationStore,
child: Portal(
child: ScaffoldMessenger(
child: MessageListCore(
paginationLimit: _config.paginationLimit,
maximumMessageLimit: _config.maximumMessageLimit,
retentionTrimBuffer: _config.retentionTrimBuffer,
messageFilter: widget.messageFilter,
loadingBuilder: defaultLoadingBuilder,
emptyBuilder: defaultEmptyBuilder,
messageListBuilder: defaultMessageListBuilder,
messageListController: _messageListController,
parentMessage: widget.parentMessage,
errorBuilder: defaultErrorBuilder,
labels: const [kPortalMessageListViewLabel],
child: Portal(
child: ScaffoldMessenger(
child: MessageListCore(
paginationLimit: _config.paginationLimit,
maximumMessageLimit: _config.maximumMessageLimit,
retentionTrimBuffer: _config.retentionTrimBuffer,
messageFilter: widget.messageFilter,
loadingBuilder: defaultLoadingBuilder,
emptyBuilder: defaultEmptyBuilder,
messageListBuilder: defaultMessageListBuilder,
messageListController: _messageListController,
parentMessage: widget.parentMessage,
errorBuilder: defaultErrorBuilder,
),
),
),
),
Expand Down Expand Up @@ -1246,7 +1255,12 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
child: Builder(
builder: (context) => switch (widget.messageBuilder) {
final builder? => builder.call(context, message, messageItemProps),
_ => StreamMessageItem.fromProps(props: messageItemProps),
// Keyed by message id so per-message local state (e.g. in the
// content/attachments/leading widgets) stays attached to the
// correct message when pagination prepends older messages and
// shifts every already-rendered item's index, rather than leaking
// onto whatever message next occupies the same position.
_ => StreamMessageItem.fromProps(key: ValueKey(message.id), props: messageItemProps),
},
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_core_flutter/chat.dart' as core;

/// Determines at which point in the [MessageListView] the initial index should
/// be.
Expand Down Expand Up @@ -161,7 +162,7 @@ StreamMessageContentKind resolveContentKind(Message message) {
}

if (!hasQuote && attachmentCount == 0) {
final emojiCount = StreamMessageText.emojiCount(message.text);
final emojiCount = core.StreamMessageText.emojiCount(message.text);
if (emojiCount != null && emojiCount <= 3) return .jumbomoji;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class StreamMessageContent extends StatefulWidget {
this.onReactionLongPress,
this.onQuotedMessageTap,
this.reactionSorting,
this.showTranslatedText = true,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

/// The message to display.
Expand Down Expand Up @@ -128,6 +129,12 @@ class StreamMessageContent extends StatefulWidget {
/// Passed through to [StreamMessageReactions.sorting].
final Comparator<ReactionGroup>? reactionSorting;

/// Whether [message] should display its translation when [Message.i18n]
/// has one for the current user's language.
///
/// Passed through to [StreamMessageText.showTranslatedText].
final bool showTranslatedText;

@override
State<StreamMessageContent> createState() => _StreamMessageContentState();
}
Expand Down Expand Up @@ -207,6 +214,7 @@ class _StreamMessageContentState extends State<StreamMessageContent> {
onLinkTap: widget.onLinkTap,
onMentionTap: widget.onMentionTap,
onAnyMentionTap: widget.onAnyMentionTap,
showTranslatedText: widget.showTranslatedText,
),
],
),
Expand Down
Loading
Loading