Merge branch 'develop' into chore/android-versions

This commit is contained in:
Rafal Adasiewicz
2023-01-25 14:45:08 +01:00
committed by GitHub
25 changed files with 967 additions and 232 deletions
@@ -8,20 +8,56 @@ Adding Push Notifications (V2) To Your Application
### Introduction
This guide details how to add push notifications to your app.
Push notifications are a core part of the experience for a messaging app. Users often need to be notified
of new messages and old notifications sometimes need to be updated silently.
This guide details how to add push notifications to your app.
Stream Chat sends push notification to channel members that have at least one registered device.
Push notifications are only sent for new messages and not for other events.
You can use [Webhooks](https://getstream.io/chat/docs/android/webhooks_overview/) to send push notifications on other types of events.
You can read more about Streams [push delivery logic](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart#push-delivery-rules).
To receive push notifications from Stream Chat, you'll need to:
1. Configure your push notification provider on the Stream Dashboard.
2. Add the client-side integration. For Flutter this guide demonstrates using Firebase Cloud Messaging (FCM).
### Push Delivery Rules
Push message delivery behaves according to these rules:
- Push notifications are sent only for new messages.
- Only channel members receive push messages.
- Members receive push notifications regardless of their online status.
- Replies inside a [thread](https://getstream.io/chat/docs/threads/) are only sent to users that are part of that thread:
- They posted at least one message
- They were mentioned
- Messages from muted users are not sent.
- Messages from muted channels are not sent.
- Messages are sent to all registered devices for a user (up to 25).
- The message doesn't contain the flag `skip_push` as true.
- `push_notifications` is enabled (default) on the channel type for message is sent.
:::info
If you would like get push notifications only when users are offline, please contact support.
:::
:::caution
Push notifications require membership. Watching a channel isn't enough.
:::
### Setup FCM
To integrate push notifications in your Flutter app, you need to use the package [firebase_messaging](https://pub.dev/packages/firebase_messaging).
Follow the [Firebase documentation](https://firebase.flutter.dev/docs/messaging/overview/) to set up the plugin for Android and iOS.
Follow the [Flutter Firebase documentation](https://firebase.flutter.dev/docs/messaging/overview/) to set up the plugin for Android and iOS.
Additional setup and instructions can be found [here](https://firebase.google.com/docs/cloud-messaging/flutter/client). Be sure to read this documentation to understand Firebase messaging functionality.
Once that's done, FCM should be able to send push notifications to your devices.
@@ -29,9 +65,9 @@ Once that's done, FCM should be able to send push notifications to your devices.
#### Step 1 - Get the Firebase Credentials
These credentials are the [private key file](https://firebase.google.com/docs/admin/setup#:~:text=To%20generate%20a%20private%20key%20file%20for%20your%20service%20account%3A) for your service account, in firebase console.
These credentials are the [private key file](https://firebase.google.com/docs/admin/setup#:~:text=To%20generate%20a%20private%20key%20file%20for%20your%20service%20account%3A) for your service account, in Firebase console.
To generate a private key file for your service account, in the Firebase console:
To generate a private key file for your service account in the Firebase console:
- Open Settings > Service Accounts.
@@ -39,7 +75,7 @@ To generate a private key file for your service account, in the Firebase console
- Securely store the JSON file containing the key.
This JSON file contains the credentials which needs to be uploaded to Streams server as explained in next step.
This JSON file contains the credentials that need to be uploaded to Streams server, as explained in the next step.
#### Step 2 - Upload the Firebase Credentials to Stream
@@ -47,11 +83,11 @@ You can upload your Firebase credentials using either the dashboard or the app s
##### Using the Stream Dashboard
1. Go to the **Chat Overview** page on Stream Dashboard
1. Go to the **Chat Overview** page on Stream Dashboard.
![](../../assets/chat_overview_page-2fbd5bbfb70c5623bd37ff7d6c41bf4d.png)
2. Enable **Firebase Notification** toggle on **Chat Overview**
2. Enable **Firebase Notification** toggle on **Chat Overview**.
![](../../assets/firebase_notifications_toggle-5aeabfcbdc24cb8f1fea7d41d0e845fc.png)
@@ -61,7 +97,7 @@ You can upload your Firebase credentials using either the dashboard or the app s
You can also enable Firebase notifications and upload the Firebase credentials using one of our server SDKs.
For example, using the JavaScript SDK:
For example, using the Stream JavaScript SDK:
```js
const client = StreamChat.getInstance('api_key', 'api_secret');
@@ -76,55 +112,56 @@ client.updateAppSettings({
),
});
```
### Registering a Device With Stream Backend
Once you configure a Firebase server key and set it up on Stream dashboard then a device that is supposed to receive push notifications needs to be registered on the Stream backend. This is usually done by listening for Firebase device token updates and passing them to the backend as follows:
Once you configure a Firebase server key and set it up on the Stream dashboard, a device that is supposed to receive push notifications needs to be registered on the Stream backend. This is usually done by listening for Firebase device token updates and passing them to the backend as follows:
```dart
firebaseMessaging.onTokenRefresh.listen((token) {
client.addDevice(token, PushProvider.firebase);
});
firebaseMessaging.onTokenRefresh.listen((token) {
client.addDevice(token, PushProvider.firebase);
});
```
Push Notifications v2 also supports specifying a name to the push device tokens you register. By setting the optional `pushProviderName` param in the `addDevice` call you can support different configurations between the device and the `PushProvider`.
Push Notifications v2 also supports specifying a name for the push device tokens you register. By setting the optional `pushProviderName` param in the `addDevice` call, you can support different configurations between the device and the `PushProvider`.
```dart
firebaseMessaging.onTokenRefresh.listen((token) {
client.addDevice(token, PushProvider.firebase, pushProviderName: 'my-custom-config');
});
firebaseMessaging.onTokenRefresh.listen((token) {
client.addDevice(token, PushProvider.firebase, pushProviderName: 'my-custom-config');
});
```
### Receiving Notifications
Push notifications behave a bit differently depending on whether you are using iOS or Android.
Push notifications behave differently depending on whether you are using iOS or Android.
See [here](https://firebase.flutter.dev/docs/messaging/usage#message-types) to understand the difference between **notification** and **data** payloads.
#### iOS
On iOS we send both a **notification** and a **data** payload.
On iOS, we send both a **notification** and a **data** payload.
This means you don't need to do anything special to get the notification to show up. However, you might want to handle the data payload to perform some logic when the user taps on the notification.
To update the template, you can use a backend SDK.
For example, using the javascript SDK:
For example, using the Stream JavaScript SDK:
```js
const client = StreamChat.getInstance(api_key, api_secret);
const apn_template = `{
"aps": {
"alert": {
"title": "New message from {{ sender.name }}",
"body": "{{ truncate message.text 2000 }}"
},
"mutable-content": 1,
"category": "stream.chat"
},
"stream": {
"sender": "stream.chat",
"aps": {
"alert": {
"title": "New message from {{ sender.name }}",
"body": "{{ truncate message.text 2000 }}"
},
"mutable-content": 1,
"category": "stream.chat"
},
"stream": {
"sender": "stream.chat",
"type": "message.new",
"version": "v2",
"id": "{{ message.id }}",
"cid": "{{ channel.cid }}"
}
}
}`;
client.updateAppSettings({
@@ -134,13 +171,23 @@ client.updateAppSettings({
```
#### Android
On Android we send only a **data** payload. This gives you more flexibility and lets you decide what to do with the notification.
On Android, we send only a **data** payload. This gives you more flexibility and lets you decide what to do with the notification.
For example, you can listen and generate a notification from them.
To generate a notification when a **data-only** message is received and the app is in background:
The code below demonstrates how to generate a notification when a **data-only** message is received and the app is in the background.
There are a few things to keep in mind about your background message handler:
1. It must not be an anonymous function.
2. It must be a top-level function (e.g. not a class method which requires initialization).
3. It must be annotated with @pragma('vm:entry-point') right above the function declaration (otherwise it may be removed during tree shaking for release mode).
For additional information on background messages, please see the [Firebase documentation](https://firebase.google.com/docs/cloud-messaging/flutter/receive#background_messages).
```dart
@pragma('vm:entry-point')
Future<void> onBackgroundMessage(RemoteMessage message) async {
final chatClient = StreamChatClient(apiKey);
@@ -164,7 +211,7 @@ void handleNotification(
final flutterLocalNotificationsPlugin = await setupLocalNotifications();
final messageId = data['id'];
final response = await chatClient.getMessage(messageId);
flutterLocalNotificationsPlugin.show(
1,
'New message from ${response.message.user.name} in ${response.channel.name}',
@@ -181,13 +228,13 @@ void handleNotification(
FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);
```
In the above example, you get the message details using the `getMessage` method and then you use the [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications) package to show the actual notification.
In the above example, you get the message details using the `getMessage` method, and then you use the [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications) package to show the actual notification.
##### Using a Template on Android
It's still possible to add a **notification** payload to Android notifications.
Adding a **notification** payload to Android notifications is still possible.
You can do so by adding a template using a backend SDK.
For example, using the javascript SDK:
For example, using the Stream JavaScript SDK:
```js
const client = StreamChat.getInstance(api_key, api_secret);
@@ -207,11 +254,12 @@ client.updateAppSettings({
### Possible Issues
Make sure to read the [general push notification docs](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart) in order to avoid known gotchas that may make your relationship with notifications difficult 😢.
Make sure to read the [general push notification docs](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart) to prevent common issues with notifications 😢.
### Testing if Push Notifications are Setup Correctly
If you're not sure whether you've set up push notifications correctly, for example, you don't always receive them, or they dont work reliably, then you can follow these steps to make sure your config is correct and working:
1. Clone our repo for push testing: `git clone [email protected]:GetStream/chat-push-test.git`
2. `cd flutter`
3. In that folder run `flutter pub get`
@@ -221,18 +269,18 @@ If you're not sure whether you've set up push notifications correctly, for examp
7. Run the app
8. Accept push notification permission (iOS only)
9. Tap on `Device ID` and copy it
11. After configuring [stream-cli](https://github.com/GetStream/stream-cli), run the following command using your user ID:
10. After configuring [stream-cli](https://github.com/GetStream/stream-cli), run the following command using your user ID:
```shell
stream chat:push:test -u <USER-ID>
stream chat:push:test -u <USER-ID>
```
You should get a test push notification 🥳
### Foreground Notifications
Sometimes you may want to show a notification when the app is in the foreground.
For example, when you're in a channel and you receive a new message from someone in another channel.
You may want to show a notification when the app is in the foreground.
For example, when you're in a channel and receive a new message from someone in another channel.
For this scenario, you can also use the `flutter_local_notifications` package to show a notification.
@@ -248,25 +296,31 @@ FirebaseMessaging.onMessage.listen((message) async {
```
:::note
You should also check that the channel of the message is different than the channel in the foreground.
You should also check that the message's channel differs from the channel in the foreground.
How you do this depends on your app infrastructure and how you handle navigation.
Take a look at the [Stream Chat v1 sample app](https://github.com/GetStream/flutter-samples/blob/main/packages/stream_chat_v1/lib/home_page.dart#L11) to see how we're doing it over there.
:::
### Saving Notification Messages to the Offline Storage (Only Android)
When the app is closed you may want to save received messages when you receive them via a notification so that later on when you open the app they're already there.
When the app is closed, you can save incoming messages when you receive them via a notification so that they're already there later when you open the app.
To do this you need to integrate the package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) in our app that exports a persistence client, see [here](https://pub.dev/packages/stream_chat_persistence#usage) how to set it up.
To do this, you need to integrate the package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) that exports a persistence client. See [here](https://pub.dev/packages/stream_chat_persistence#usage) for information on how to set it up.
Then calling `FirebaseMessaging.onBackgroundMessage(...)` you need to use a TOP-LEVEL or STATIC function to handle background messages; here is an example:
Then calling `FirebaseMessaging.onBackgroundMessage(...)` you need to use a TOP-LEVEL or STATIC function to handle background messages.
For additional information on background messages, please see the [Firebase documentation](https://firebase.google.com/docs/cloud-messaging/flutter/receive#background_messages).
Here is an example:
```dart
@pragma('vm:entry-point')
Future<void> onBackgroundMessage(RemoteMessage message) async {
final chatClient = StreamChatClient(apiKey);
final persistenceClient = StreamChatPersistenceClient();
await persistenceClient.connect(userId);
final persistenceClient = StreamChatPersistenceClient();
await persistenceClient.connect(userId);
chatClient.connectUser(
User(id: userId),
@@ -287,9 +341,9 @@ void handleNotification(
final messageId = data['id'];
final cid = data['cid'];
final response = await chatClient.getMessage(messageId);
await persistenceClient.updateMessages(cid, [response.message]);
persistenceClient.disconnect();
await persistenceClient.updateMessages(cid, [response.message]);
persistenceClient.disconnect();
flutterLocalNotificationsPlugin.show(
1,
@@ -306,4 +360,3 @@ void handleNotification(
FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);
```
+10
View File
@@ -1,3 +1,13 @@
## 5.2.0
✅ Added
- Added `Huawei` and `Xiaomi` PushProviders.
🐞 Fixed
- Fixed initializing last synced date.
## 5.1.0
✅ Added
@@ -453,6 +453,15 @@ class StreamChatClient {
if (persistenceEnabled) {
await sync(cids: cids, lastSyncAt: _lastSyncedAt);
}
} else {
// channels are empty, assuming it's a fresh start
// and making sure `lastSyncAt` is initialized
if (persistenceEnabled) {
final lastSyncAt = await _chatPersistenceClient?.getLastSyncAt();
if (lastSyncAt == null) {
await _chatPersistenceClient?.updateLastSyncAt(DateTime.now());
}
}
}
handleEvent(Event(
type: EventType.connectionRecovered,
@@ -6,6 +6,12 @@ enum PushProvider {
/// Send notifications using Google's Firebase Cloud Messaging
firebase,
/// Send notifications using Huawei's Push Kit
huawei,
/// Send notifications using Xiaomi's Mi Push Service
xiaomi,
/// Send notifications using Apple's Push Notification service
apn,
}
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names
const PACKAGE_VERSION = '5.1.0';
const PACKAGE_VERSION = '5.2.0';
+1 -1
View File
@@ -1,7 +1,7 @@
name: stream_chat
homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications.
version: 5.1.0
version: 5.2.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -516,6 +516,9 @@ void main() {
});
setUp(() async {
when(() => persistence.updateLastSyncAt(any()))
.thenAnswer((_) => Future.value());
when(persistence.getLastSyncAt).thenAnswer((_) async => null);
client = StreamChatClient(apiKey, chatApi: api, ws: ws)
..chatPersistenceClient = persistence;
await client.connectUser(user, token);
@@ -532,9 +535,12 @@ void main() {
test(
'''should update persistence connectionInfo and lastSync when sync succeeds''',
() async {
// persistence.updateLastSyncAt might be called
// when connecting the user.
// Resetting the logs so we start counting invocations correctly.
reset(persistence);
const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3'];
final lastSyncAt = DateTime.now();
when(() => api.general.sync(cids, lastSyncAt))
.thenAnswer((_) async => SyncResponse()
..events = [
@@ -567,6 +573,10 @@ void main() {
test(
'should work fine if persistence contains sync params',
() async {
// persistence.updateLastSyncAt might be called
// when connecting the user.
// Resetting the logs so we start counting invocations correctly.
reset(persistence);
const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3'];
final lastSyncAt = DateTime.now();
@@ -22,26 +22,37 @@ void main() {
test('addDevice should work', () async {
const deviceId = 'test-device-id';
const pushProvider = PushProvider.firebase;
const pushProvidersMap = {
'apn': PushProvider.apn,
'firebase': PushProvider.firebase,
'huawei': PushProvider.huawei,
'xiaomi': PushProvider.xiaomi,
};
const path = '/devices';
when(() => client.post(
path,
data: {
'id': deviceId,
'push_provider': pushProvider.name,
},
))
.thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{}));
for (final pushProviderMapEntry in pushProvidersMap.entries) {
final data = {
'id': deviceId,
'push_provider': pushProviderMapEntry.key,
};
when(() {
return client.post(
path,
data: data,
);
}).thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{}));
final res = await deviceApi.addDevice(deviceId, pushProvider);
final res =
await deviceApi.addDevice(deviceId, pushProviderMapEntry.value);
expect(res, isNotNull);
expect(res, isNotNull);
verify(() => client.post(path, data: any(named: 'data'))).called(1);
verify(() => client.post(path, data: data)).called(1);
}
verifyNoMoreInteractions(client);
expect(pushProvidersMap.length, PushProvider.values.length,
reason: 'All PushProvider should be tested');
});
test('addDevice should work with pushProviderName', () async {
+16 -2
View File
@@ -1,10 +1,24 @@
## Upcomming
## Upcoming
🐞 Fixed
- [[#1424]](https://github.com/GetStream/stream-chat-flutter/issues/1424) Fixed a render issue when showing messages starting with 4 whitespaces.
- Fixed a bug where the `AttachmentPickerBottomSheet` was not able to identify the mobile browser.
## 5.2.0
✅ Added
- Added a new `bottomRowBuilderWithDefaultWidget` parameter to `StreamMessageWidget` which contains a third parameter (default `BottomRow` widget with `copyWith` method available) to allow easier customization.
🔄 Changed
- Updated `lottie` dependency to `^2.0.0`
- Updated `desktop_drop` dependency to `^0.4.0`
- Updated `connectivity_plus` dependency to `^3.0.2`
- Updated `dart_vlc` dependency to `^0.4.0`
- Updated `file_picker` dependency to `^5.2.4`
- Deprecated `StreamMessageWidget.bottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
- Deprecated `StreamMessageWidget.deletedBottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
- Deprecated `StreamMessageWidget.usernameBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
🐞 Fixed
- [[#1379]](https://github.com/GetStream/stream-chat-flutter/issues/1379) Fixed "Issues with photo attachments on web", where the cached image attachment would not render while uploading.
@@ -1318,4 +1332,4 @@ The property showVideoFullScreen was added mainly because of this issue brianega
## 0.0.1
- First release
- First release
@@ -8,7 +8,6 @@
#include <dart_vlc/dart_vlc_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
@@ -21,9 +20,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
@@ -5,7 +5,6 @@
list(APPEND FLUTTER_PLUGIN_LIST
dart_vlc
desktop_drop
file_selector_linux
screen_retriever
sqlite3_flutter_libs
url_launcher_linux
@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform;
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Shows a modal material design bottom sheet.
@@ -107,44 +107,34 @@ Future<T?> showStreamAttachmentPickerModalBottomSheet<T>({
controller: controller,
initialAttachments: initialAttachments,
builder: (context, controller, child) {
return PlatformWidget(
web: (context) {
return webOrDesktopAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions?.map(
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
),
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
mobile: (context) {
return mobileAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions,
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
desktop: (context) {
return webOrDesktopAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions?.map(
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
),
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
final currentPlatform = defaultTargetPlatform;
final isWebOrDesktop = kIsWeb ||
currentPlatform == TargetPlatform.macOS ||
currentPlatform == TargetPlatform.linux ||
currentPlatform == TargetPlatform.windows;
if (isWebOrDesktop) {
return webOrDesktopAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions?.map(
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
),
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
}
return mobileAttachmentPickerBuilder.call(
context: context,
controller: controller,
customOptions: customOptions,
attachmentThumbnailSize: attachmentThumbnailSize,
attachmentThumbnailFormat: attachmentThumbnailFormat,
attachmentThumbnailQuality: attachmentThumbnailQuality,
attachmentThumbnailScale: attachmentThumbnailScale,
);
},
);
@@ -32,6 +32,7 @@ class BottomRow extends StatelessWidget {
this.deletedBottomRowBuilder,
this.onThreadTap,
this.usernameBuilder,
this.sendingIndicatorBuilder,
});
/// {@macro messageIsDeleted}
@@ -88,6 +89,61 @@ class BottomRow extends StatelessWidget {
/// {@macro usernameBuilder}
final Widget Function(BuildContext, Message)? usernameBuilder;
/// {@macro sendingIndicatorBuilder}
final Widget Function(BuildContext, Message)? sendingIndicatorBuilder;
/// {@template copyWith}
/// Creates a copy of [BottomRow] with specified attributes
/// overridden.
/// {@endtemplate}
BottomRow copyWith({
Key? key,
bool? isDeleted,
Message? message,
bool? showThreadReplyIndicator,
bool? showInChannel,
bool? showTimeStamp,
bool? showUsername,
bool? reverse,
bool? showSendingIndicator,
bool? hasUrlAttachments,
bool? isGiphy,
bool? isOnlyEmoji,
StreamMessageThemeData? messageTheme,
StreamChatThemeData? streamChatTheme,
bool? hasNonUrlAttachments,
StreamChatState? streamChat,
Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
void Function(Message)? onThreadTap,
Widget Function(BuildContext, Message)? usernameBuilder,
Widget Function(BuildContext, Message)? sendingIndicatorBuilder,
}) =>
BottomRow(
key: key ?? this.key,
isDeleted: isDeleted ?? this.isDeleted,
message: message ?? this.message,
showThreadReplyIndicator:
showThreadReplyIndicator ?? this.showThreadReplyIndicator,
showInChannel: showInChannel ?? this.showInChannel,
showTimeStamp: showTimeStamp ?? this.showTimeStamp,
showUsername: showUsername ?? this.showUsername,
reverse: reverse ?? this.reverse,
showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator,
hasUrlAttachments: hasUrlAttachments ?? this.hasUrlAttachments,
isGiphy: isGiphy ?? this.isGiphy,
isOnlyEmoji: isOnlyEmoji ?? this.isOnlyEmoji,
messageTheme: messageTheme ?? this.messageTheme,
streamChatTheme: streamChatTheme ?? this.streamChatTheme,
hasNonUrlAttachments: hasNonUrlAttachments ?? this.hasNonUrlAttachments,
streamChat: streamChat ?? this.streamChat,
deletedBottomRowBuilder:
deletedBottomRowBuilder ?? this.deletedBottomRowBuilder,
onThreadTap: onThreadTap ?? this.onThreadTap,
usernameBuilder: usernameBuilder ?? this.usernameBuilder,
sendingIndicatorBuilder:
sendingIndicatorBuilder ?? this.sendingIndicatorBuilder,
);
@override
Widget build(BuildContext context) {
if (isDeleted) {
@@ -147,13 +203,14 @@ class BottomRow extends StatelessWidget {
),
if (showSendingIndicator)
WidgetSpan(
child: SendingIndicatorWrapper(
messageTheme: messageTheme,
message: message,
hasNonUrlAttachments: hasNonUrlAttachments,
streamChat: streamChat,
streamChatTheme: streamChatTheme,
),
child: sendingIndicatorBuilder?.call(context, message) ??
SendingIndicatorWrapper(
messageTheme: messageTheme,
message: message,
hasNonUrlAttachments: hasNonUrlAttachments,
streamChat: streamChat,
streamChatTheme: streamChatTheme,
),
),
]);
@@ -40,7 +40,8 @@ class StreamMessageText extends StatelessWidget {
.translate(language)
.replaceMentions()
.text
?.replaceAll('\n', '\n\n');
?.replaceAll('\n', '\n\n')
.trim();
final themeData = Theme.of(context);
return MarkdownBody(
data: messageText ?? '',
@@ -8,6 +8,7 @@ import 'package:stream_chat_flutter/src/context_menu_items/context_menu_reaction
import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart';
import 'package:stream_chat_flutter/src/dialogs/dialogs.dart';
import 'package:stream_chat_flutter/src/message_actions_modal/message_actions_modal.dart';
import 'package:stream_chat_flutter/src/message_widget/bottom_row.dart';
import 'package:stream_chat_flutter/src/message_widget/message_widget_content.dart';
import 'package:stream_chat_flutter/src/message_widget/reactions/message_reactions_modal.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -79,8 +80,15 @@ class StreamMessageWidget extends StatefulWidget {
this.userAvatarBuilder,
this.editMessageInputBuilder,
this.textBuilder,
this.bottomRowBuilder,
this.deletedBottomRowBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.bottomRowBuilder,
this.bottomRowBuilderWithDefaultWidget,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.deletedBottomRowBuilder,
this.customAttachmentBuilders,
this.padding,
this.textPadding = const EdgeInsets.symmetric(
@@ -92,11 +100,18 @@ class StreamMessageWidget extends StatefulWidget {
this.onQuotedMessageTap,
this.customActions = const [],
this.onAttachmentTap,
this.usernameBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.usernameBuilder,
this.imageAttachmentThumbnailSize = const Size(400, 400),
this.imageAttachmentThumbnailResizeType = 'clip',
this.imageAttachmentThumbnailCropType = 'center',
}) : attachmentBuilders = {
}) : assert(
bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null,
'You can only use one of the two bottom row builders',
),
attachmentBuilders = {
'image': (context, message, attachments) {
final border = RoundedRectangleBorder(
side: attachmentBorderSide ??
@@ -306,7 +321,13 @@ class StreamMessageWidget extends StatefulWidget {
/// {@template bottomRowBuilder}
/// Widget builder for building a bottom row below the message
/// {@endtemplate}
final Widget Function(BuildContext, Message)? bottomRowBuilder;
final BottomRowBuilder? bottomRowBuilder;
/// {@template bottomRowBuilderWithDefaultWidget}
/// Widget builder for building a bottom row below the message.
/// Also contains the default bottom row widget.
/// {@endtemplate}
final BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget;
/// {@template deletedBottomRowBuilder}
/// Widget builder for building a bottom row below a deleted message
@@ -537,9 +558,19 @@ class StreamMessageWidget extends StatefulWidget {
void Function(Message)? onReplyTap,
Widget Function(BuildContext, Message)? editMessageInputBuilder,
Widget Function(BuildContext, Message)? textBuilder,
Widget Function(BuildContext, Message)? usernameBuilder,
Widget Function(BuildContext, Message)? bottomRowBuilder,
Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') Widget Function(BuildContext, Message)? usernameBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') BottomRowBuilder? bottomRowBuilder,
BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
void Function(BuildContext, Message)? onMessageActions,
Message? message,
StreamMessageThemeData? messageTheme,
@@ -587,6 +618,29 @@ class StreamMessageWidget extends StatefulWidget {
String? imageAttachmentThumbnailResizeType,
String? imageAttachmentThumbnailCropType,
}) {
assert(
bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null,
'You can only use one of the two bottom row builders',
);
var _bottomRowBuilderWithDefaultWidget =
bottomRowBuilderWithDefaultWidget ??
this.bottomRowBuilderWithDefaultWidget;
_bottomRowBuilderWithDefaultWidget ??= (context, message, defaultWidget) {
final _bottomRowBuilder = bottomRowBuilder ?? this.bottomRowBuilder;
if (_bottomRowBuilder != null) {
return _bottomRowBuilder(context, message);
}
return defaultWidget.copyWith(
onThreadTap: onThreadTap ?? this.onThreadTap,
usernameBuilder: usernameBuilder ?? this.usernameBuilder,
deletedBottomRowBuilder:
deletedBottomRowBuilder ?? this.deletedBottomRowBuilder,
);
};
return StreamMessageWidget(
key: key ?? this.key,
onMentionTap: onMentionTap ?? this.onMentionTap,
@@ -595,10 +649,7 @@ class StreamMessageWidget extends StatefulWidget {
editMessageInputBuilder:
editMessageInputBuilder ?? this.editMessageInputBuilder,
textBuilder: textBuilder ?? this.textBuilder,
usernameBuilder: usernameBuilder ?? this.usernameBuilder,
bottomRowBuilder: bottomRowBuilder ?? this.bottomRowBuilder,
deletedBottomRowBuilder:
deletedBottomRowBuilder ?? this.deletedBottomRowBuilder,
bottomRowBuilderWithDefaultWidget: _bottomRowBuilderWithDefaultWidget,
onMessageActions: onMessageActions ?? this.onMessageActions,
message: message ?? this.message,
messageTheme: messageTheme ?? this.messageTheme,
@@ -838,52 +889,69 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
? Alignment.centerRight
: Alignment.centerLeft,
widthFactor: widget.widthFactor,
child: MessageWidgetContent(
streamChatTheme: _streamChatTheme,
showUsername: showUsername,
showTimeStamp: showTimeStamp,
showThreadReplyIndicator: showThreadReplyIndicator,
showSendingIndicator: showSendingIndicator,
showInChannel: showInChannel,
isGiphy: isGiphy,
isOnlyEmoji: isOnlyEmoji,
hasUrlAttachments: hasUrlAttachments,
messageTheme: widget.messageTheme,
reverse: widget.reverse,
message: widget.message,
hasNonUrlAttachments: hasNonUrlAttachments,
shouldShowReactions: shouldShowReactions,
hasQuotedMessage: hasQuotedMessage,
textPadding: widget.textPadding,
attachmentBuilders: widget.attachmentBuilders,
attachmentPadding: widget.attachmentPadding,
avatarWidth: avatarWidth,
bottomRowPadding: bottomRowPadding,
isFailedState: isFailedState,
isPinned: isPinned,
messageWidget: widget,
showBottomRow: showBottomRow,
showPinHighlight: widget.showPinHighlight,
showReactionPickerIndicator:
widget.showReactionPickerIndicator,
showReactions: showReactions,
showUserAvatar: widget.showUserAvatar,
streamChat: _streamChat,
translateUserAvatar: widget.translateUserAvatar,
deletedBottomRowBuilder: widget.deletedBottomRowBuilder,
onThreadTap: widget.onThreadTap,
shape: widget.shape,
borderSide: widget.borderSide,
borderRadiusGeometry: widget.borderRadiusGeometry,
textBuilder: widget.textBuilder,
onLinkTap: widget.onLinkTap,
onMentionTap: widget.onMentionTap,
onQuotedMessageTap: widget.onQuotedMessageTap,
bottomRowBuilder: widget.bottomRowBuilder,
onUserAvatarTap: widget.onUserAvatarTap,
userAvatarBuilder: widget.userAvatarBuilder,
usernameBuilder: widget.usernameBuilder,
),
child: Builder(builder: (context) {
var _bottomRowBuilderWithDefaultWidget =
widget.bottomRowBuilderWithDefaultWidget;
_bottomRowBuilderWithDefaultWidget ??=
(context, message, defaultWidget) {
final _bottomRowBuilder = widget.bottomRowBuilder;
if (_bottomRowBuilder != null) {
return _bottomRowBuilder(context, message);
}
return defaultWidget.copyWith(
onThreadTap: widget.onThreadTap,
usernameBuilder: widget.usernameBuilder,
deletedBottomRowBuilder: widget.deletedBottomRowBuilder,
);
};
return MessageWidgetContent(
streamChatTheme: _streamChatTheme,
showUsername: showUsername,
showTimeStamp: showTimeStamp,
showThreadReplyIndicator: showThreadReplyIndicator,
showSendingIndicator: showSendingIndicator,
showInChannel: showInChannel,
isGiphy: isGiphy,
isOnlyEmoji: isOnlyEmoji,
hasUrlAttachments: hasUrlAttachments,
messageTheme: widget.messageTheme,
reverse: widget.reverse,
message: widget.message,
hasNonUrlAttachments: hasNonUrlAttachments,
shouldShowReactions: shouldShowReactions,
hasQuotedMessage: hasQuotedMessage,
textPadding: widget.textPadding,
attachmentBuilders: widget.attachmentBuilders,
attachmentPadding: widget.attachmentPadding,
avatarWidth: avatarWidth,
bottomRowPadding: bottomRowPadding,
isFailedState: isFailedState,
isPinned: isPinned,
messageWidget: widget,
showBottomRow: showBottomRow,
showPinHighlight: widget.showPinHighlight,
showReactionPickerIndicator:
widget.showReactionPickerIndicator,
showReactions: showReactions,
showUserAvatar: widget.showUserAvatar,
streamChat: _streamChat,
translateUserAvatar: widget.translateUserAvatar,
shape: widget.shape,
borderSide: widget.borderSide,
borderRadiusGeometry: widget.borderRadiusGeometry,
textBuilder: widget.textBuilder,
onLinkTap: widget.onLinkTap,
onMentionTap: widget.onMentionTap,
onQuotedMessageTap: widget.onQuotedMessageTap,
bottomRowBuilderWithDefaultWidget:
_bottomRowBuilderWithDefaultWidget,
onUserAvatarTap: widget.onUserAvatarTap,
userAvatarBuilder: widget.userAvatarBuilder,
);
}),
),
),
),
@@ -4,6 +4,18 @@ import 'package:stream_chat_flutter/src/message_widget/message_widget_content_co
import 'package:stream_chat_flutter/src/message_widget/reactions/desktop_reactions_builder.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Signature for the builder function that will be called when the message
/// bottom row is built. Includes the [Message].
typedef BottomRowBuilder = Widget Function(BuildContext, Message);
/// Signature for the builder function that will be called when the message
/// bottom row is built. Includes the [Message] and the default [BottomRow].
typedef BottomRowBuilderWithDefaultWidget = Widget Function(
BuildContext,
Message,
BottomRow,
);
/// {@template messageWidgetContent}
/// The main content of a [StreamMessageWidget].
///
@@ -51,12 +63,28 @@ class MessageWidgetContent extends StatelessWidget {
this.onMentionTap,
this.onLinkTap,
this.textBuilder,
this.bottomRowBuilder,
this.onThreadTap,
this.deletedBottomRowBuilder,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.bottomRowBuilder,
this.bottomRowBuilderWithDefaultWidget,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.onThreadTap,
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.deletedBottomRowBuilder,
this.userAvatarBuilder,
this.usernameBuilder,
});
@Deprecated('''
Use [bottomRowBuilderWithDefaultWidget] instead.
Will be removed in the next major version.
''') this.usernameBuilder,
}) : assert(
bottomRowBuilder == null || bottomRowBuilderWithDefaultWidget == null,
'You can only use one of the two bottom row builders',
);
/// {@macro reverse}
final bool reverse;
@@ -152,7 +180,10 @@ class MessageWidgetContent extends StatelessWidget {
final double bottomRowPadding;
/// {@macro bottomRowBuilder}
final Widget Function(BuildContext, Message)? bottomRowBuilder;
final BottomRowBuilder? bottomRowBuilder;
/// {@macro bottomRowBuilderWithDefaultWidget}
final BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget;
/// {@macro showInChannelIndicator}
final bool showInChannel;
@@ -207,30 +238,7 @@ class MessageWidgetContent extends StatelessWidget {
right: reverse ? bottomRowPadding : 0,
bottom: isPinned && showPinHighlight ? 6.0 : 0.0,
),
child: bottomRowBuilder?.call(
context,
message,
) ??
BottomRow(
message: message,
reverse: reverse,
messageTheme: messageTheme,
hasUrlAttachments: hasUrlAttachments,
isOnlyEmoji: isOnlyEmoji,
isDeleted: message.isDeleted,
isGiphy: isGiphy,
showInChannel: showInChannel,
showSendingIndicator: showSendingIndicator,
showThreadReplyIndicator: showThreadReplyIndicator,
showTimeStamp: showTimeStamp,
showUsername: showUsername,
streamChatTheme: streamChatTheme,
onThreadTap: onThreadTap,
deletedBottomRowBuilder: deletedBottomRowBuilder,
streamChat: streamChat,
hasNonUrlAttachments: hasNonUrlAttachments,
usernameBuilder: usernameBuilder,
),
child: _buildBottomRow(context),
),
Padding(
padding: EdgeInsets.only(
@@ -457,4 +465,39 @@ class MessageWidgetContent extends StatelessWidget {
),
);
}
Widget _buildBottomRow(BuildContext context) {
final defaultWidget = BottomRow(
message: message,
reverse: reverse,
messageTheme: messageTheme,
hasUrlAttachments: hasUrlAttachments,
isOnlyEmoji: isOnlyEmoji,
isDeleted: message.isDeleted,
isGiphy: isGiphy,
showInChannel: showInChannel,
showSendingIndicator: showSendingIndicator,
showThreadReplyIndicator: showThreadReplyIndicator,
showTimeStamp: showTimeStamp,
showUsername: showUsername,
streamChatTheme: streamChatTheme,
onThreadTap: onThreadTap,
deletedBottomRowBuilder: deletedBottomRowBuilder,
streamChat: streamChat,
hasNonUrlAttachments: hasNonUrlAttachments,
usernameBuilder: usernameBuilder,
);
if (bottomRowBuilder != null) {
return bottomRowBuilder!(context, message);
} else if (bottomRowBuilderWithDefaultWidget != null) {
return bottomRowBuilderWithDefaultWidget!(
context,
message,
defaultWidget,
);
}
return defaultWidget;
}
}
+5 -5
View File
@@ -1,12 +1,12 @@
name: stream_chat_flutter
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
version: 5.1.0
version: 5.2.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
environment:
sdk: '>=2.17.0 <3.0.0'
sdk: ">=2.17.0 <3.0.0"
flutter: ">=1.17.0"
dependencies:
@@ -14,12 +14,12 @@ dependencies:
chewie: ^1.3.4
collection: ^1.15.0
contextmenu: ^3.0.0
dart_vlc: ^0.3.0
dart_vlc: ^0.4.0
desktop_drop: ^0.4.0
diacritic: ^0.1.3
dio: ^4.0.6
ezanimation: ^0.6.0
file_picker: ^4.1.3
file_picker: ^5.2.4
file_selector: ^0.9.0
flutter:
sdk: flutter
@@ -39,7 +39,7 @@ dependencies:
rxdart: ^0.27.0
share_plus: ^4.5.0
shimmer: ^2.0.0
stream_chat_flutter_core: ^5.1.0
stream_chat_flutter_core: ^5.2.0
synchronized: ^3.0.0
thumblr: ^0.0.4
url_launcher: ^6.1.0
@@ -163,7 +163,7 @@ void main() {
);
testWidgets(
'tapping on reply should pop',
'tapping on reply should invoke callback',
(WidgetTester tester) async {
final client = MockClient();
final clientState = MockClientState();
@@ -174,7 +174,7 @@ void main() {
final themeData = ThemeData();
final streamTheme = StreamChatThemeData.fromTheme(themeData);
final mockObserver = MockNavigatorObserver();
final mockCallback = MockVoidCallback();
final attachment = Attachment(
type: 'image',
@@ -192,7 +192,6 @@ void main() {
await tester.pumpWidget(
MaterialApp(
theme: themeData,
navigatorObservers: [mockObserver],
home: StreamChat(
streamChatThemeData: streamTheme,
client: client,
@@ -200,13 +199,14 @@ void main() {
child: AttachmentActionsModal(
message: message,
attachment: attachment,
onReply: mockCallback,
),
),
),
),
);
await tester.tap(find.text('Reply'));
verify(() => mockObserver.didPop(any(), any()));
verify(mockCallback.call);
},
);
@@ -1,4 +1,4 @@
## Upcomming
## 5.2.0
🔄 Changed
@@ -1,7 +1,7 @@
name: stream_chat_flutter_core
homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
version: 5.1.0
version: 5.2.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -17,7 +17,7 @@ dependencies:
freezed_annotation: ^2.0.3
meta: ^1.3.0
rxdart: ^0.27.0
stream_chat: ^5.1.0
stream_chat: ^5.2.0
dev_dependencies:
build_runner: ^2.0.1
dart_code_metrics: ^4.4.0
@@ -1,3 +1,13 @@
## Upcoming
✅ Added
* Added support for [Catalan](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart) locale.
🔄 Changed
* Some of the `Spanish` translations have been updated/changed for better understanding.
## 4.0.0
🔄 Changed
@@ -35,6 +35,7 @@ At the moment we support the following languages:
- [Italian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart)
- [French](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart)
- [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart)
- [Catalan](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart)
- [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart)
- [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart)
- [Portuguese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart)
@@ -75,6 +76,7 @@ class MyApp extends StatelessWidget {
Locale('fr'),
Locale('it'),
Locale('es'),
Locale('ca'),
Locale('ja'),
Locale('ko'),
Locale('pt'),
@@ -123,6 +125,7 @@ Example:
<string>fr</string>
<string>it</string>
<string>es</string>
<string>ca</string>
<string>ja</string>
<string>ko</string>
<string>pt</string>
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
part 'stream_chat_localizations_ca.dart';
part 'stream_chat_localizations_de.dart';
part 'stream_chat_localizations_en.dart';
part 'stream_chat_localizations_es.dart';
@@ -28,6 +29,7 @@ const kStreamChatSupportedLanguages = {
'fr',
'it',
'es',
'ca',
'ja',
'ko',
'pt',
@@ -65,6 +67,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) {
return const StreamChatLocalizationsIt();
case 'es':
return const StreamChatLocalizationsEs();
case 'ca':
return const StreamChatLocalizationsCa();
case 'ja':
return const StreamChatLocalizationsJa();
case 'ko':
@@ -0,0 +1,451 @@
part of 'stream_chat_localizations.dart';
/// The translations for Catalan (`ca`).
class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations {
/// Create an instance of the translation bundle for Catalan.
const StreamChatLocalizationsCa({super.localeName = 'ca'});
@override
String get launchUrlError => "No s'ha pogut obrir la url";
@override
String get loadingUsersError => "Error de càrrega de l'usuari";
@override
String get noUsersLabel => 'Actualment no hi ha usuaris';
@override
String get retryLabel => 'Torna-ho a provar';
@override
String get userLastOnlineText => 'Última vegada en línia';
@override
String get userOnlineText => 'En línia';
@override
String userTypingText(Iterable<User> users) {
if (users.isEmpty) return '';
final first = users.first;
if (users.length == 1) {
return '${first.name} està escrivint';
}
return '${first.name} y ${users.length - 1} estan escrivint';
}
@override
String get threadReplyLabel => 'Respon al fil';
@override
String get onlyVisibleToYouText => 'Només visible per vostè';
@override
String threadReplyCountText(int count) => '$count respostes al fil';
@override
String attachmentsUploadProgressText({
required int remaining,
required int total,
}) =>
'Transferència en curs $remaining/$total ...';
@override
String pinnedByUserText({
required User pinnedBy,
required User currentUser,
}) {
final pinnedByCurrentUser = currentUser.id == pinnedBy.id;
if (pinnedByCurrentUser) return 'Fixat per tu';
return 'Fixat per ${pinnedBy.name}';
}
@override
String get sendMessagePermissionError =>
'No tens permís per enviar missatges';
@override
String get emptyMessagesText => 'Actualment no hi ha missatges';
@override
String get genericErrorText => 'Hi ha hagut un problema';
@override
String get loadingMessagesError =>
'Hi ha hagut un error mentres carregava el missatge';
@override
String resultCountText(int count) => '$count resultats';
@override
String get messageDeletedText => 'Aquest missatge ha estat esborrat.';
@override
String get messageDeletedLabel => 'Missatge esborrat';
@override
String get messageReactionsLabel => 'Reaccions dels missatges';
@override
String get emptyChatMessagesText => 'Encara no hi ha missatges...';
@override
String threadSeparatorText(int replyCount) {
if (replyCount == 1) return '1 resposta';
return '$replyCount respostes';
}
@override
String get connectedLabel => 'Connectat';
@override
String get disconnectedLabel => 'Desconnectat';
@override
String get reconnectingLabel => 'Reconnectant...';
@override
String get alsoSendAsDirectMessageLabel =>
'Enviar també com a missatge directe';
@override
String get addACommentOrSendLabel => 'Afegir un comentari o enviar';
@override
String get searchGifLabel => 'Cerca de GIFs';
@override
String get writeAMessageLabel => 'Escriure un missatge';
@override
String get instantCommandsLabel => 'Commandes instantànies';
@override
String fileTooLargeAfterCompressionError(double limitInMB) =>
'El fitxer és massa gran descargar-lo. '
'La mida màxima del fitxer és de $limitInMB MB. '
'Hem intentat comprimir-lo, pero ha estat suficient.';
@override
String fileTooLargeError(double limitInMB) =>
'El fitxer és massa gran per descargar-lo. '
'El límit de mida dels fitxers és de $limitInMB MB.';
@override
String get couldNotReadBytesFromFileError =>
"No s'han pogut llegir els bytes del fitxer.";
@override
String get addAFileLabel => 'Afegir un fitxer';
@override
String get photoFromCameraLabel => 'Foto de la càmera';
@override
String get uploadAFileLabel => 'Transferir un fitxer';
@override
String get uploadAPhotoLabel => 'Pujar una foto';
@override
String get uploadAVideoLabel => 'Pujar un vídeo';
@override
String get videoFromCameraLabel => 'Vídeo de la càmera';
@override
String get okLabel => 'Vale';
@override
String get somethingWentWrongError => 'Alguna cosa ha anat malament';
@override
String get addMoreFilesLabel => 'Afegir més fitxers';
@override
String get enablePhotoAndVideoAccessMessage =>
"Si us plau, permeti l'accés a les seves fotos"
'\ni vídeos per a que pugui compartir-los.';
@override
String get allowGalleryAccessMessage => "Permetre l'accés a la galeria";
@override
String get flagMessageLabel => 'Reportar un missatge';
@override
String get flagMessageQuestion =>
"¿Vol enviar una còpia d'aquest missatge a un"
'\nmoderador per una major investigació?';
@override
String get flagLabel => 'REPORTAR';
@override
String get cancelLabel => 'CANCELAR';
@override
String get flagMessageSuccessfulLabel => 'Missatge reportat';
@override
String get flagMessageSuccessfulText =>
'Aquest missatge ha estat reportat a un moderador.';
@override
String get deleteLabel => 'ESBORRAR';
@override
String get deleteMessageLabel => 'Esborrar el missatge';
@override
String get deleteMessageQuestion =>
'¿Estàs segur de que vols esborrar aquest\nmissatge de forma permanent?';
@override
String get operationCouldNotBeCompletedText =>
"L'operació no s'ha pogut completar.";
@override
String get replyLabel => 'Respondre';
@override
String togglePinUnpinText({required bool pinned}) {
if (pinned) return 'Desfixar de la conversa';
return 'Fixar a la conversa';
}
@override
String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) {
if (isDeleteFailed) return 'Reintentar esborrar el misssatge';
return 'Esborrar el misssatge';
}
@override
String get copyMessageLabel => 'Copiar el misssatge';
@override
String get editMessageLabel => 'Editar el misssatge';
@override
String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) {
if (isUpdateFailed) return 'Reenviar el missatge modificat';
return 'Reenviar';
}
@override
String get photosLabel => 'Fotos';
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 'avui';
} else if (date == yesterday) {
return 'ahir';
} else {
return 'el ${Jiffy(date).MMMd}';
}
}
@override
String sentAtText({required DateTime date, required DateTime time}) =>
'''Enviat el ${_getDay(date)} a les ${Jiffy(time.toLocal()).format('HH:mm')}''';
@override
String get todayLabel => 'Avui';
@override
String get yesterdayLabel => 'Ahir';
@override
String get channelIsMutedText => 'El canal està silenciat';
@override
String get noTitleText => 'Sense títol';
@override
String get letsStartChattingLabel => '¡Comencem a parlar!';
@override
String get sendingFirstMessageLabel =>
'Qué li sembla enviar el seu primer missatge a un amic?';
@override
String get startAChatLabel => 'Iniciar una conversa';
@override
String get loadingChannelsError => 'Error al carregar els canals';
@override
String get deleteConversationLabel => 'Esborrar la conversa';
@override
String get deleteConversationQuestion =>
'Estàs segur de que vols esborrar aquesta conversa?';
@override
String get streamChatLabel => 'Stream Chat';
@override
String get searchingForNetworkText => 'Buscant xarxa';
@override
String get offlineLabel => 'Sense connexió...';
@override
String get tryAgainLabel => 'Torna-ho a provar';
@override
String membersCountText(int count) {
if (count == 1) return '1 membre';
return '$count membres';
}
@override
String watchersCountText(int count) {
if (count == 1) return '1 En línea';
return '$count En línea';
}
@override
String get viewInfoLabel => 'Veure informació';
@override
String get leaveGroupLabel => 'Sortir del Grup';
@override
String get leaveLabel => 'SORTIR';
@override
String get leaveConversationLabel => 'Sortir de la conversa';
@override
String get leaveConversationQuestion =>
"Estàs segur de que vol sortir d'aquesta conversa?";
@override
String get showInChatLabel => 'Mostrar al chat';
@override
String get saveImageLabel => 'Guardar la imatge';
@override
String get saveVideoLabel => 'Guardar el vídeo';
@override
String get uploadErrorLabel => 'ERROR DE TRANSFERENCIA';
@override
String get giphyLabel => 'Giphy';
@override
String get shuffleLabel => 'Remenar';
@override
String get sendLabel => 'Enviar';
@override
String get withText => 'amb';
@override
String get inText => 'a';
@override
String get youText => 'Vostè';
@override
String galleryPaginationText({
required int currentPage,
required int totalPages,
}) =>
'${currentPage + 1} de $totalPages';
@override
String get fileText => 'Fitxer';
@override
String get replyToMessageLabel => 'Respondre al missatge';
@override
String attachmentLimitExceedError(int limit) =>
'No és possible afegir més de $limit fitxers adjunts';
@override
String get viewLibrary => 'Veure llibreria';
@override
String get slowModeOnLabel => 'Mode lent activat';
@override
String get downloadLabel => 'Descarregar';
@override
String toggleMuteUnmuteUserText({required bool isMuted}) {
if (isMuted) {
return "Activar so de l'usuari";
} else {
return 'Silenciar usuari';
}
}
@override
String toggleMuteUnmuteGroupQuestion({required bool isMuted}) {
if (isMuted) {
return "Estàs segur de que vols activar el so d'aquest grup?";
} else {
return 'Estàs segur de que vols silenciar aquest grup?';
}
}
@override
String toggleMuteUnmuteUserQuestion({required bool isMuted}) {
if (isMuted) {
return "Estàs segur de que vols activar el so d'aquest usuari";
} else {
return 'Estàs seguro de que vols silenciar aquest usuari?';
}
}
@override
String toggleMuteUnmuteAction({required bool isMuted}) {
if (isMuted) {
return 'ACTIVAR SO';
} else {
return 'SILENCIAR';
}
}
@override
String toggleMuteUnmuteGroupText({required bool isMuted}) {
if (isMuted) {
return 'Activar so del grup';
} else {
return 'Silenciar grup';
}
}
@override
String get linkDisabledDetails =>
'No es permet enviar enllaços a aquesta conversa.';
@override
String get linkDisabledError => 'Els enllaços estan deshabilitats';
@override
String unreadMessagesSeparatorText(int unreadCount) {
if (unreadCount == 1) {
return '1 missatge no llegit';
}
return '$unreadCount missatges no llegits';
}
@override
String get enableFileAccessMessage => "Habiliti l'accés als fitxers"
'\nper poder compartir-los amb amics.';
@override
String get allowFileAccessMessage => "Permetre l'accés als fitxers";
}
@@ -84,7 +84,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations {
String get messageDeletedLabel => 'Mensaje borrado';
@override
String get messageReactionsLabel => 'Reacciones a los mensajes';
String get messageReactionsLabel => 'Reacciones de los mensajes';
@override
String get emptyChatMessagesText => 'Todavía no hay charlas aquí...';
@@ -165,7 +165,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations {
@override
String get enablePhotoAndVideoAccessMessage =>
'Por favor, permita el acceso a sus fotos'
'\ny vídeos para que puedas compartirlos con sus amigos.';
'\ny vídeos para que pueda compartirlos con sus amigos.';
@override
String get allowGalleryAccessMessage => 'Permitir el acceso a su galería';
@@ -206,11 +206,11 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations {
'La operación no pudo completarse.';
@override
String get replyLabel => 'Respuesta';
String get replyLabel => 'Responder';
@override
String togglePinUnpinText({required bool pinned}) {
if (pinned) return 'Desfijar a la conversación';
if (pinned) return 'Desfijar de la conversación';
return 'Fijar a la conversación';
}
@@ -389,14 +389,14 @@ No es posible añadir más de $limit archivos adjuntos
if (isMuted) {
return 'No silenciar usuario';
} else {
return 'Usuario mudo';
return 'Silenciar usuario';
}
}
@override
String toggleMuteUnmuteGroupQuestion({required bool isMuted}) {
if (isMuted) {
return '¿Estás seguro de que quieres activar el silencio de este grupo?';
return '¿Estás seguro de que quieres activar el sonido de este grupo?';
} else {
return '¿Estás seguro de que quieres silenciar a este grupo?';
}