Merge branch 'develop' into cds-189
This commit is contained in:
@@ -21,6 +21,7 @@ jobs:
|
||||
ui
|
||||
doc
|
||||
repo
|
||||
localization
|
||||
requireScope: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -48,6 +48,9 @@ This package provides business logic to fetch common things required for integra
|
||||
### [stream_chat_flutter](https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat_flutter)
|
||||
This library includes both a low-level chat SDK and a set of reusable and customizable UI components.
|
||||
|
||||
### [stream_chat_localizations](https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat_localizations)
|
||||
This library includes a set of localization files for the Flutter UI components.
|
||||
|
||||
## Flutter Chat Tutorial
|
||||
|
||||
The best place to start is the [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/).
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -28,6 +28,7 @@ The core package allows more customisation and hence provides business logic but
|
||||
reusable and customisable UI components.
|
||||
4. <b>Persistence (stream_chat_persistence)</b>: provides a persistence client for fetching and
|
||||
saving chat data locally.
|
||||
5. <b>Localizations (stream_chat_localizations)</b>: provides a set of localizations for the SDK.
|
||||
|
||||
We recommend building prototypes using the full UI package since it contains UI widgets already
|
||||
integrated with Stream's API. [stream_chat_flutter](https://pub.dev/packages/stream_chat_flutter)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
id: adding_custom_attachments
|
||||
sidebar_position: 2
|
||||
sidebar_position: 3
|
||||
title: Adding Custom Attachments
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
id: adding_localization
|
||||
sidebar_position: 2
|
||||
title: Adding Localization
|
||||
---
|
||||
|
||||
Adding Localization To UI Widgets
|
||||
|
||||
### Introduction
|
||||
|
||||
We have a dedicated package for adding localization to our UI widgets. It's called `stream_chat_localizations` and you can find it [here](https://pub.dev/packages/stream_chat_localizations).
|
||||
|
||||

|
||||
|
||||
## What is Localization?
|
||||
|
||||
If you deploy your app to users who speak another language, you'll need to internationalize (localize) it. That means you need to write the app in a way that makes it possible to localize values like text and layouts for each language or locale that the app supports. For more information, see the [Flutter documentation](https://flutter.dev/docs/development/accessibility-and-localization/**internationalization**).
|
||||
|
||||
What this package allows you to do is to provide localized strings for the Stream chat widgets. For example, depending on the application locale, the Stream Chat widgets will display the appropriate language. The locale will be set automatically, based on system preferences, or you could set it programmatically in your app. The package supports several different languages, with more to be added. The package allows you to override any supported language or add a new language that isn't supported.
|
||||
|
||||
### Supported languages
|
||||
|
||||
At the moment we support the following languages:
|
||||
- [English](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart)
|
||||
- [Hindi](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart)
|
||||
- [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)
|
||||
|
||||
More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages.
|
||||
|
||||
### Add dependency
|
||||
|
||||
Add this to your package's `pubspec.yaml` file, use the latest version [](https://pub.dartlang.org/packages/stream_chat_localizations)
|
||||
```yaml
|
||||
dependencies:
|
||||
stream_chat_localizations: ^latest_version
|
||||
```
|
||||
|
||||
Then run `flutter packages get`
|
||||
|
||||
### Usage
|
||||
|
||||
Generally, Flutter and the Stream Chat SDK will use the system locale of the user's device, if that locale is supported (see below). If the locale is not supported we will default to `en` (however it's always possible to [customize that](#changing-the-default-language)).
|
||||
Make sure to read more about localization in the [official Flutter docs](https://flutter.dev/docs/development/accessibility-and-localization/internationalization).
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
// Add all the supported locales
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('hi'),
|
||||
Locale('fr'),
|
||||
Locale('it'),
|
||||
],
|
||||
// Add GlobalStreamChatLocalizations.delegates
|
||||
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
|
||||
builder: (context, widget) => StreamChat(
|
||||
client: client,
|
||||
child: widget,
|
||||
),
|
||||
home: StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Setting a language
|
||||
The application language can be changed through system preferences or programmatically.
|
||||
|
||||
### System Preferences
|
||||
The application locale can be changed by changing the language for your device or emulator within the device's system preferences.
|
||||
|
||||
[iOS change language](https://support.apple.com/en-us/HT204031)
|
||||
|
||||
[Android change language](https://support.google.com/websearch/answer/3333234?co=GENIE.Platform%3DAndroid&hl=en)
|
||||
|
||||
Note that the language needs to be supported in your application to work.
|
||||
|
||||
### Programmatically
|
||||
You can also set the locale programmatically in your Flutter application without changing the device's language.
|
||||
|
||||
```dart
|
||||
return MaterialApp(
|
||||
...
|
||||
locale: const Locale('fr'),
|
||||
...
|
||||
);
|
||||
```
|
||||
|
||||
There are many ways that this can be set for additional control. For information and examples, see this [Stack Overflow post](https://stackoverflow.com/questions/49441212/flutter-multi-lingual-application-how-to-override-the-locale).
|
||||
|
||||
### Adding a new language
|
||||
|
||||
To add a new language, create a new class extending `GlobalStreamChatLocalizations` and create a delegate for it, adding it to the `delegates` array.
|
||||
|
||||
Check out [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/add_new_lang.dart) to see how to add a new language.
|
||||
|
||||
### Override existing languages
|
||||
|
||||
To override an existing language, create a new class extending that particular language class and create a delegate for it, adding it to the `delegates` array.
|
||||
|
||||
Check out [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/override_lang.dart) to see how to override an existing language.
|
||||
|
||||
### Changing the default language
|
||||
|
||||
To change the default language you can use the `MaterialApp.localeListResolutionCallback` property.
|
||||
Here is an example of how that would look like:
|
||||
|
||||
```dart
|
||||
MaterialApp(
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
// Add all the supported locales
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('hi'),
|
||||
Locale('fr'),
|
||||
Locale('it'),
|
||||
],
|
||||
// locales are the locales of the device
|
||||
// supportedLocales are the app supported locales
|
||||
localeListResolutionCallback: (locales, supportedLocales) {
|
||||
// We map the supported locales to language codes
|
||||
// note that this is completely optional and this logic can be changed as you like
|
||||
final supportedLanguageCodes =
|
||||
supportedLocales.map((e) => e.languageCode);
|
||||
if (locales != null) {
|
||||
// we iterate over the locales and find the first one that is supported
|
||||
for (final locale in locales) {
|
||||
if (supportedLanguageCodes.contains(locale.languageCode)) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we didn't find a supported language, we return the Italian language
|
||||
return const Locale('it');
|
||||
},
|
||||
// Add GlobalStreamChatLocalizations.delegates
|
||||
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
In this case, we're using Italian as the default language.
|
||||
|
||||
### ⚠️ Note on **iOS**
|
||||
|
||||
For translation to work on **iOS** you need to add supported locales to
|
||||
`ios/Runner/Info.plist` as described [here](https://flutter.dev/docs/development/accessibility-and-localization/internationalization#specifying-supportedlocales).
|
||||
|
||||
Example:
|
||||
|
||||
```xml
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>nb</string>
|
||||
<string>fr</string>
|
||||
<string>it</string>
|
||||
</array>
|
||||
```
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
id: customize_text_messages
|
||||
sidebar_position: 6
|
||||
title: Customize Text Messages
|
||||
---
|
||||
|
||||
Customizing Text Messages
|
||||
|
||||
### Introduction
|
||||
|
||||
Every application provides a unique look and feel to their own messaging interface including and not
|
||||
limited to fonts, colors, and shapes.
|
||||
|
||||
This guide details how to customize message text in the `MessageListView` / `MessageWidget` in the
|
||||
Stream Chat Flutter UI SDK.
|
||||
|
||||
:::note
|
||||
This guide is specifically for the `MessageListView` but if you intend to display a `MessageWidget`
|
||||
separately, follow the same process without the `.copyWith` and use the default constructor instead.
|
||||
:::
|
||||
|
||||
### Basics of customizing a `MessageWidget`
|
||||
|
||||
First, add a `MessageListView` in the appropriate place where you intend to display messages from a
|
||||
channel.
|
||||
|
||||
```dart
|
||||
MessageListView(
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
Now, we use the `messageBuilder` parameter to build a custom message. The builder function also provides
|
||||
the default implementation of the `MessageWidget` so that we can change certain aspects of the widget
|
||||
without redoing all of the default parameters.
|
||||
|
||||
:::note
|
||||
In earlier versions of the SDK, some `MessageWidget` parameters were exposed directly through the `MessageListView`,
|
||||
however, this quickly becomes hard to maintain as more parameters and customizations are added to the
|
||||
`MessageWidget`. Newer version utilise a cleaner interface to change the parameters by supplying a
|
||||
default message implementation as aforementioned.
|
||||
:::
|
||||
|
||||
```dart
|
||||
MessageListView(
|
||||
...
|
||||
messageBuilder: (context, messageDetails, messageList, defaultWidget) {
|
||||
return defaultWidget;
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
We use `.copyWith()` to customize the widget:
|
||||
|
||||
```dart
|
||||
MessageListView(
|
||||
...
|
||||
messageBuilder: (context, messageDetails, messageList, defaultWidget) {
|
||||
return defaultWidget.copyWith(
|
||||
...
|
||||
);
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Customizing text
|
||||
|
||||
If you intend to simply change the theme for the text, you need not recreate the whole widget. The
|
||||
`MessageWidget` has a `messageTheme` parameter that allows you to pass the theme for most aspects
|
||||
of the message.
|
||||
|
||||
```dart
|
||||
MessageListView(
|
||||
...
|
||||
messageBuilder: (context, messageDetails, messageList, defaultWidget) {
|
||||
return defaultWidget.copyWith(
|
||||
messageTheme: MessageTheme(
|
||||
...
|
||||
messageText: TextStyle(),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
If you want to replace the entire text widget in the `MessageWidget`, you can use the `textBuilder`
|
||||
parameter which provides a builder for creating a widget to substitute the default text.parameter
|
||||
|
||||
```dart
|
||||
MessageListView(
|
||||
...
|
||||
messageBuilder: (context, messageDetails, messageList, defaultWidget) {
|
||||
return defaultWidget.copyWith(
|
||||
textBuilder: (context, message) {
|
||||
return Text(message.text);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Adding Hashtags
|
||||
|
||||
To add elements like hashtags, we can override the `textBuilder` in the MessageWidget:
|
||||
|
||||
```dart
|
||||
MessageListView(
|
||||
...
|
||||
messageBuilder: (context, messageDetails, messageList, defaultWidget) {
|
||||
return defaultWidget.copyWith(
|
||||
textBuilder: (context, message) {
|
||||
final text = _replaceHashtags(message.text).replaceAll('\n', '\\\n');
|
||||
final messageTheme = StreamChatTheme.of(context).ownMessageTheme;
|
||||
|
||||
return MarkdownBody(
|
||||
data: text,
|
||||
onTapLink: (
|
||||
String link,
|
||||
String href,
|
||||
String title,
|
||||
) {
|
||||
// Do something with tapped hashtag
|
||||
},
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||
Theme.of(context).copyWith(
|
||||
textTheme: Theme.of(context).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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
String _replaceHashtags(String text) {
|
||||
RegExp exp = new RegExp(r"\B#\w\w+");
|
||||
exp.allMatches(text).forEach((match){
|
||||
text = text.replaceAll(
|
||||
'${match.group(0)}', '[${match.group(0)}](${match.group(0).replaceAll(' ', '')})');
|
||||
});
|
||||
return text;
|
||||
}
|
||||
```
|
||||
|
||||
We can replace the hashtags using RegEx and add links for the MarkdownBody which is done here in the
|
||||
`_replaceHashtags()` function.
|
||||
Inside the textBuilder, we use the `flutter_markdown` package to build our hashtags as links.
|
||||
|
||||

|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
id: mig_guide_2_0
|
||||
sidebar_position: 3
|
||||
sidebar_position: 4
|
||||
title: Migrating to 2.0 (Null-safety)
|
||||
---
|
||||
|
||||
@@ -57,7 +57,7 @@ The changes will be listed by package and a concise changelog will follow with m
|
||||
|
||||
* `MessageBuilder` and `ParentMessageBuilder` signature is now
|
||||
|
||||
```
|
||||
```dart
|
||||
typedef MessageBuilder = Widget Function(
|
||||
BuildContext,
|
||||
MessageDetails,
|
||||
|
||||
+4
-4
@@ -13,7 +13,7 @@ scripts:
|
||||
|
||||
analyze:
|
||||
run: |
|
||||
melos exec -c 4 --ignore="*example*" -- \
|
||||
melos exec -c 5 --ignore="*example*" -- \
|
||||
dart analyze --fatal-infos .
|
||||
description: |
|
||||
Run `dart analyze` in all packages.
|
||||
@@ -26,7 +26,7 @@ scripts:
|
||||
|
||||
lint:pub:
|
||||
run: |
|
||||
melos exec -c 4 --no-private --ignore="*example*" -- \
|
||||
melos exec -c 5 --no-private --ignore="*example*" -- \
|
||||
pub publish --dry-run
|
||||
description: |
|
||||
Run `pub publish --dry-run` in all packages.
|
||||
@@ -56,7 +56,7 @@ scripts:
|
||||
dir-exists: test
|
||||
|
||||
test:flutter:
|
||||
run: melos exec -c 3 --fail-fast -- "flutter test --coverage"
|
||||
run: melos exec -c 4 --fail-fast -- "flutter test --coverage"
|
||||
description: Run Flutter tests for a specific package in this project.
|
||||
select-package:
|
||||
flutter: true
|
||||
@@ -64,7 +64,7 @@ scripts:
|
||||
|
||||
coverage:ignore-file:
|
||||
run: |
|
||||
melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from-coverage.sh"
|
||||
melos exec -c 5 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from-coverage.sh"
|
||||
description: Removes all the ignored files from the coverage report.
|
||||
select-package:
|
||||
dir-exists: coverage
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
## 2.1.1
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Mutes were not working correctly in 2.1.0
|
||||
|
||||
## 2.1.0
|
||||
|
||||
🛑️ Removed
|
||||
|
||||
- The `MessageTranslation` class has been removed. Use the new `i18n` field in the `Message` class instead.
|
||||
|
||||
✅ Added
|
||||
|
||||
- The `Message` class now has an `i18n` field for translations
|
||||
- The `User` class now has a `language` field for the user's language preference.
|
||||
|
||||
🔄 Changed
|
||||
|
||||
- `client.user` is now deprecated in favor of `client.currentUser`.
|
||||
- `client.userStream` is now deprecated in favor of `client.currentUserStream`.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [#563](https://github.com/GetStream/stream-chat-flutter/issues/563): `Channel.stopWatching()` not working
|
||||
- [#575](https://github.com/GetStream/stream-chat-flutter/issues/575): Wrong `OwnUser.*`
|
||||
|
||||
## 2.0.0
|
||||
|
||||
🛑️ Breaking Changes from `1.5.3`
|
||||
@@ -619,4 +646,4 @@
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- first beta version
|
||||
- first beta version
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- [Register](https://getstream.io/chat/trial/) to get an API key for Stream Chat
|
||||
- [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/)
|
||||
- [Chat UI Kit](https://getstream.io/chat/ui-kit/)
|
||||
- [Chat Client Docs](https://getstream.io/chat/docs/flutter-dart/?language=dart)
|
||||
|
||||
### Changelog
|
||||
|
||||
|
||||
@@ -245,5 +245,5 @@ class _MessageViewState extends State<MessageView> {
|
||||
/// Helper extension for quickly retrieving
|
||||
/// the current user id from a [StreamChatClient].
|
||||
extension on StreamChatClient {
|
||||
String get uid => state.user!.id;
|
||||
String get uid => state.currentUser!.id;
|
||||
}
|
||||
|
||||
@@ -65,12 +65,12 @@ class Channel {
|
||||
|
||||
/// Returns true if the channel is muted
|
||||
bool get isMuted =>
|
||||
_client.state.user?.channelMutes
|
||||
_client.state.currentUser?.channelMutes
|
||||
.any((element) => element.channel.cid == cid) ==
|
||||
true;
|
||||
|
||||
/// Returns true if the channel is muted as a stream
|
||||
Stream<bool>? get isMutedStream => _client.state.userStream
|
||||
Stream<bool>? get isMutedStream => _client.state.currentUserStream
|
||||
.map((event) =>
|
||||
event!.channelMutes.any((element) => element.channel.cid == cid) ==
|
||||
true)
|
||||
@@ -397,7 +397,7 @@ class Channel {
|
||||
// ignore: parameter_assignments
|
||||
message = message.copyWith(
|
||||
createdAt: message.createdAt,
|
||||
user: _client.state.user,
|
||||
user: _client.state.currentUser,
|
||||
quotedMessage: quotedMessage,
|
||||
status: MessageSendingStatus.sending,
|
||||
attachments: message.attachments.map(
|
||||
@@ -711,7 +711,7 @@ class Channel {
|
||||
_checkInitialized();
|
||||
final messageId = message.id;
|
||||
final now = DateTime.now();
|
||||
final user = _client.state.user;
|
||||
final user = _client.state.currentUser;
|
||||
|
||||
final latestReactions = [...message.latestReactions ?? <Reaction>[]];
|
||||
if (enforceUnique) {
|
||||
@@ -768,7 +768,7 @@ class Channel {
|
||||
Future<EmptyResponse> deleteReaction(
|
||||
Message message, Reaction reaction) async {
|
||||
final type = reaction.type;
|
||||
final user = _client.state.user;
|
||||
final user = _client.state.currentUser;
|
||||
|
||||
final reactionCounts = {...message.reactionCounts ?? <String, int>{}};
|
||||
if (reactionCounts.containsKey(type)) {
|
||||
@@ -1346,7 +1346,7 @@ class ChannelClientState {
|
||||
|
||||
void _computeInitialUnread() {
|
||||
final userRead = channelState.read.firstWhereOrNull(
|
||||
(r) => r.user.id == _channel._client.state.user?.id,
|
||||
(r) => r.user.id == _channel._client.state.currentUser?.id,
|
||||
);
|
||||
if (userRead != null) {
|
||||
unreadCount = userRead.unreadMessages;
|
||||
@@ -1463,7 +1463,7 @@ class ChannelClientState {
|
||||
|
||||
void _listenReactionDeleted() {
|
||||
_subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) {
|
||||
final userId = _channel.client.state.user!.id;
|
||||
final userId = _channel.client.state.currentUser!.id;
|
||||
final message = event.message!.copyWith(
|
||||
ownReactions: [...event.message!.latestReactions!]
|
||||
..removeWhere((it) => it.userId != userId),
|
||||
@@ -1474,7 +1474,7 @@ class ChannelClientState {
|
||||
|
||||
void _listenReactions() {
|
||||
_subscriptions.add(_channel.on(EventType.reactionNew).listen((event) {
|
||||
final userId = _channel.client.state.user!.id;
|
||||
final userId = _channel.client.state.currentUser!.id;
|
||||
final message = event.message!.copyWith(
|
||||
ownReactions: [...event.message!.latestReactions!]
|
||||
..removeWhere((it) => it.userId != userId),
|
||||
@@ -1490,7 +1490,7 @@ class ChannelClientState {
|
||||
EventType.reactionUpdated,
|
||||
)
|
||||
.listen((event) {
|
||||
final userId = _channel.client.state.user!.id;
|
||||
final userId = _channel.client.state.currentUser!.id;
|
||||
final message = event.message!.copyWith(
|
||||
ownReactions: [...event.message!.latestReactions!]
|
||||
..removeWhere((it) => it.userId != userId),
|
||||
@@ -1584,7 +1584,7 @@ class ChannelClientState {
|
||||
|
||||
if (userReadIndex != null && userReadIndex != -1) {
|
||||
final userRead = readList.removeAt(userReadIndex);
|
||||
if (userRead.user.id == _channel._client.state.user!.id) {
|
||||
if (userRead.user.id == _channel._client.state.currentUser!.id) {
|
||||
unreadCount = 0;
|
||||
}
|
||||
readList.add(Read(
|
||||
@@ -1674,11 +1674,12 @@ class ChannelClientState {
|
||||
int get unreadCount => _unreadCountController.value;
|
||||
|
||||
bool _countMessageAsUnread(Message message) {
|
||||
final userId = _channel.client.state.user?.id;
|
||||
final userIsMuted = _channel.client.state.user?.mutes.firstWhereOrNull(
|
||||
(m) => m.user.id == message.user?.id,
|
||||
) !=
|
||||
null;
|
||||
final userId = _channel.client.state.currentUser?.id;
|
||||
final userIsMuted =
|
||||
_channel.client.state.currentUser?.mutes.firstWhereOrNull(
|
||||
(m) => m.user.id == message.user?.id,
|
||||
) !=
|
||||
null;
|
||||
return message.silent != true &&
|
||||
message.shadowed != true &&
|
||||
message.user?.id != userId &&
|
||||
@@ -1827,7 +1828,7 @@ class ChannelClientState {
|
||||
(event) {
|
||||
if (event.user != null) {
|
||||
final user = event.user!;
|
||||
if (user.id != _channel.client.state.user?.id) {
|
||||
if (user.id != _channel.client.state.currentUser?.id) {
|
||||
_typings[user] = event;
|
||||
_typingEventsController.add(_typings);
|
||||
}
|
||||
@@ -1840,7 +1841,7 @@ class ChannelClientState {
|
||||
(event) {
|
||||
if (event.user != null) {
|
||||
final user = event.user!;
|
||||
if (user.id != _channel.client.state.user?.id) {
|
||||
if (user.id != _channel.client.state.currentUser?.id) {
|
||||
_typings.remove(event.user);
|
||||
_typingEventsController.add(_typings);
|
||||
}
|
||||
|
||||
@@ -25,12 +25,14 @@ import 'package:stream_chat/src/core/models/member.dart';
|
||||
import 'package:stream_chat/src/core/models/message.dart';
|
||||
import 'package:stream_chat/src/core/models/own_user.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||
import 'package:stream_chat/src/core/util/utils.dart';
|
||||
import 'package:stream_chat/src/db/chat_persistence_client.dart';
|
||||
import 'package:stream_chat/src/event_type.dart';
|
||||
import 'package:stream_chat/src/location.dart';
|
||||
import 'package:stream_chat/src/ws/connection_status.dart';
|
||||
import 'package:stream_chat/src/ws/websocket.dart';
|
||||
import 'package:stream_chat/version.dart';
|
||||
|
||||
/// Handler function used for logging records. Function requires a single
|
||||
/// [LogRecord] as the only parameter.
|
||||
@@ -42,6 +44,10 @@ final _levelEmojiMapper = {
|
||||
Level.SEVERE: '🚨',
|
||||
};
|
||||
|
||||
final _userAgent = 'stream-chat-dart-client-'
|
||||
'${CurrentPlatform.name}-'
|
||||
'${PACKAGE_VERSION.split('+')[0]}';
|
||||
|
||||
/// The official Dart client for Stream Chat,
|
||||
/// a service for building chat applications.
|
||||
/// This library can be used on any Dart project and on both mobile and web apps
|
||||
@@ -80,6 +86,7 @@ class StreamChatClient {
|
||||
location: location,
|
||||
connectTimeout: connectTimeout,
|
||||
receiveTimeout: receiveTimeout,
|
||||
headers: {'X-Stream-Client': _userAgent},
|
||||
);
|
||||
|
||||
_chatApi = chatApi ??
|
||||
@@ -99,6 +106,7 @@ class StreamChatClient {
|
||||
tokenManager: _tokenManager,
|
||||
handler: handleEvent,
|
||||
logger: detachedLogger('🔌'),
|
||||
queryParameters: {'X-Stream-Client': _userAgent},
|
||||
);
|
||||
|
||||
_retryPolicy = retryPolicy ??
|
||||
@@ -308,23 +316,21 @@ class StreamChatClient {
|
||||
);
|
||||
|
||||
final ownUser = OwnUser.fromUser(user);
|
||||
state.user = ownUser;
|
||||
state.currentUser = ownUser;
|
||||
|
||||
if (!connectWebSocket) {
|
||||
return ownUser;
|
||||
}
|
||||
if (!connectWebSocket) return ownUser;
|
||||
|
||||
try {
|
||||
if (_originalChatPersistenceClient != null) {
|
||||
_chatPersistenceClient = _originalChatPersistenceClient;
|
||||
await _chatPersistenceClient!.connect(ownUser.id);
|
||||
}
|
||||
final res = await openConnection();
|
||||
return res;
|
||||
final connectedUser = await openConnection();
|
||||
return state.currentUser = connectedUser;
|
||||
} catch (e, stk) {
|
||||
if (e is StreamWebSocketError && e.isRetriable) {
|
||||
final event = await _chatPersistenceClient?.getConnectionInfo();
|
||||
if (event != null) return event.me?.merge(ownUser) ?? ownUser;
|
||||
if (event != null) return ownUser.merge(event.me);
|
||||
}
|
||||
logger.severe('error connecting user : ${ownUser.id}', e, stk);
|
||||
rethrow;
|
||||
@@ -334,12 +340,12 @@ class StreamChatClient {
|
||||
/// Creates a new WebSocket connection with the current user.
|
||||
Future<OwnUser> openConnection() async {
|
||||
assert(
|
||||
state.user != null,
|
||||
state.currentUser != null,
|
||||
'User is not set on client, '
|
||||
'use `connectUser` or `connectAnonymousUser` instead',
|
||||
);
|
||||
|
||||
final user = state.user!;
|
||||
final user = state.currentUser!;
|
||||
|
||||
logger.info('Opening web-socket connection for ${user.id}');
|
||||
|
||||
@@ -363,7 +369,7 @@ class StreamChatClient {
|
||||
|
||||
try {
|
||||
final event = await _ws.connect(user);
|
||||
return event.me?.merge(user) ?? user;
|
||||
return user.merge(event.me);
|
||||
} catch (e, stk) {
|
||||
logger.severe('error connecting ws', e, stk);
|
||||
rethrow;
|
||||
@@ -378,7 +384,7 @@ class StreamChatClient {
|
||||
void closeConnection() {
|
||||
if (wsConnectionStatus == ConnectionStatus.disconnected) return;
|
||||
|
||||
logger.info('Closing web-socket connection for ${state.user?.id}');
|
||||
logger.info('Closing web-socket connection for ${state.currentUser?.id}');
|
||||
_wsConnectionStatus = ConnectionStatus.disconnected;
|
||||
|
||||
_connectionStatusSubscription?.cancel();
|
||||
@@ -388,9 +394,6 @@ class StreamChatClient {
|
||||
}
|
||||
|
||||
void _handleHealthCheckEvent(Event event) {
|
||||
final user = event.me;
|
||||
if (user != null) state.user = user;
|
||||
|
||||
final connectionId = event.connectionId;
|
||||
if (connectionId != null) {
|
||||
_connectionIdManager.setConnectionId(connectionId);
|
||||
@@ -1312,7 +1315,7 @@ class StreamChatClient {
|
||||
/// If [flushChatPersistence] is true the client deletes all offline
|
||||
/// user's data.
|
||||
Future<void> disconnectUser({bool flushChatPersistence = false}) async {
|
||||
logger.info('Disconnecting user : ${state.user?.id}');
|
||||
logger.info('Disconnecting user : ${state.currentUser?.id}');
|
||||
|
||||
// resetting state
|
||||
state.dispose();
|
||||
@@ -1355,16 +1358,15 @@ class ClientState {
|
||||
_subscriptions.addAll([
|
||||
_client
|
||||
.on()
|
||||
.where((event) => event.me != null)
|
||||
.map((e) => e.me)
|
||||
.where((event) =>
|
||||
event.me != null && event.type != EventType.healthCheck)
|
||||
.map((e) => e.me!)
|
||||
.listen((user) {
|
||||
_userController.add(user);
|
||||
final totalUnreadCount = user?.totalUnreadCount;
|
||||
if (totalUnreadCount != null) {
|
||||
_totalUnreadCountController.add(totalUnreadCount);
|
||||
}
|
||||
currentUser = currentUser?.merge(user) ?? user;
|
||||
final totalUnreadCount = user.totalUnreadCount;
|
||||
_totalUnreadCountController.add(totalUnreadCount);
|
||||
|
||||
final unreadChannels = user?.unreadChannels;
|
||||
final unreadChannels = user.unreadChannels;
|
||||
if (unreadChannels != null) {
|
||||
_unreadChannelsController.add(unreadChannels);
|
||||
}
|
||||
@@ -1408,8 +1410,8 @@ class ClientState {
|
||||
|
||||
void _listenUserUpdated() {
|
||||
_subscriptions.add(_client.on(EventType.userUpdated).listen((event) {
|
||||
if (event.user!.id == user!.id) {
|
||||
user = OwnUser.fromJson(event.user!.toJson());
|
||||
if (event.user!.id == currentUser!.id) {
|
||||
currentUser = OwnUser.fromJson(event.user!.toJson());
|
||||
}
|
||||
updateUser(event.user);
|
||||
}));
|
||||
@@ -1431,9 +1433,10 @@ class ClientState {
|
||||
|
||||
final StreamChatClient _client;
|
||||
|
||||
/// Update user information
|
||||
set user(OwnUser? user) {
|
||||
_userController.add(user);
|
||||
/// Sets the user currently interacting with the client
|
||||
/// note: this fully overrides the [currentUser]
|
||||
set currentUser(OwnUser? user) {
|
||||
_currentUserController.add(user);
|
||||
}
|
||||
|
||||
/// Update all the [users] with the provided [userList]
|
||||
@@ -1450,10 +1453,24 @@ class ClientState {
|
||||
void updateUser(User? user) => updateUsers([user]);
|
||||
|
||||
/// The current user
|
||||
OwnUser? get user => _userController.valueOrNull;
|
||||
OwnUser? get currentUser => _currentUserController.valueOrNull;
|
||||
|
||||
/// The current user as a stream
|
||||
Stream<OwnUser?> get userStream => _userController.stream;
|
||||
Stream<OwnUser?> get currentUserStream => _currentUserController.stream;
|
||||
|
||||
// coverage:ignore-start
|
||||
|
||||
/// The current user
|
||||
@Deprecated('Use `.currentUser` instead, Will be removed in future releases')
|
||||
OwnUser? get user => _currentUserController.valueOrNull;
|
||||
|
||||
/// The current user as a stream
|
||||
@Deprecated(
|
||||
'Use `.currentUserStream` instead, Will be removed in future releases',
|
||||
)
|
||||
Stream<OwnUser?> get userStream => _currentUserController.stream;
|
||||
|
||||
// coverage:ignore-end
|
||||
|
||||
/// The current user
|
||||
Map<String, User> get users => _usersController.value;
|
||||
@@ -1485,7 +1502,7 @@ class ClientState {
|
||||
}
|
||||
|
||||
final _channelsController = BehaviorSubject<Map<String, Channel>>.seeded({});
|
||||
final _userController = BehaviorSubject<OwnUser?>();
|
||||
final _currentUserController = BehaviorSubject<OwnUser?>();
|
||||
final _usersController = BehaviorSubject<Map<String, User>>.seeded({});
|
||||
final _unreadChannelsController = BehaviorSubject<int>.seeded(0);
|
||||
final _totalUnreadCountController = BehaviorSubject<int>.seeded(0);
|
||||
@@ -1493,7 +1510,7 @@ class ClientState {
|
||||
/// Call this method to dispose this object
|
||||
void dispose() {
|
||||
_subscriptions.forEach((s) => s.cancel());
|
||||
_userController.close();
|
||||
_currentUserController.close();
|
||||
_unreadChannelsController.close();
|
||||
_totalUnreadCountController.close();
|
||||
channels.values.forEach((c) => c.dispose());
|
||||
|
||||
@@ -75,7 +75,7 @@ class QueryChannelsResponse extends _BaseResponse {
|
||||
@JsonSerializable(createToJson: false)
|
||||
class TranslateMessageResponse extends _BaseResponse {
|
||||
/// Translated message
|
||||
late TranslatedMessage message;
|
||||
late Message message;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static TranslateMessageResponse fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -47,8 +47,7 @@ TranslateMessageResponse _$TranslateMessageResponseFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return TranslateMessageResponse()
|
||||
..duration = json['duration'] as String?
|
||||
..message =
|
||||
TranslatedMessage.fromJson(json['message'] as Map<String, dynamic>);
|
||||
..message = Message.fromJson(json['message'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
QueryMembersResponse _$QueryMembersResponseFromJson(Map<String, dynamic> json) {
|
||||
|
||||
@@ -10,9 +10,7 @@ import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.
|
||||
import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart';
|
||||
import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart';
|
||||
import 'package:stream_chat/src/core/http/token_manager.dart';
|
||||
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||
import 'package:stream_chat/src/location.dart';
|
||||
import 'package:stream_chat/version.dart';
|
||||
|
||||
part 'stream_http_client_options.dart';
|
||||
|
||||
@@ -33,11 +31,14 @@ class StreamHttpClient {
|
||||
..options.baseUrl = _options.baseUrl
|
||||
..options.receiveTimeout = _options.receiveTimeout.inMilliseconds
|
||||
..options.connectTimeout = _options.connectTimeout.inMilliseconds
|
||||
..options.queryParameters = {'api_key': apiKey}
|
||||
..options.queryParameters = {
|
||||
'api_key': apiKey,
|
||||
..._options.queryParameters,
|
||||
}
|
||||
..options.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Stream-Client': _options.userAgent,
|
||||
'Content-Encoding': 'application/gzip',
|
||||
..._options.headers,
|
||||
}
|
||||
..interceptors.addAll([
|
||||
if (tokenManager != null) AuthInterceptor(this, tokenManager),
|
||||
|
||||
@@ -10,6 +10,8 @@ class StreamHttpClientOptions {
|
||||
this.location,
|
||||
this.connectTimeout = const Duration(seconds: 6),
|
||||
this.receiveTimeout = const Duration(seconds: 6),
|
||||
this.queryParameters = const {},
|
||||
this.headers = const {},
|
||||
}) : _baseUrl = baseUrl ?? _defaultBaseURL;
|
||||
|
||||
final String _baseUrl;
|
||||
@@ -32,8 +34,20 @@ class StreamHttpClientOptions {
|
||||
/// received timeout, default to 6s
|
||||
final Duration receiveTimeout;
|
||||
|
||||
/// Get the current user agent
|
||||
String get userAgent => 'stream-chat-dart-client-'
|
||||
'${CurrentPlatform.name}-'
|
||||
'${PACKAGE_VERSION.split('+')[0]}';
|
||||
/// Common query parameters.
|
||||
///
|
||||
/// List values use the default [ListFormat.multiCompatible].
|
||||
///
|
||||
/// The value can be overridden per parameter by adding a [MultiParam]
|
||||
/// object wrapping the actual List value and the desired format.
|
||||
final Map<String, Object?> queryParameters;
|
||||
|
||||
/// Http request headers.
|
||||
/// The keys of initial headers will be converted to lowercase,
|
||||
/// for example 'Content-Type' will be converted to 'content-type'.
|
||||
///
|
||||
/// The key of Header Map is case-insensitive
|
||||
/// eg: content-type and Content-Type are
|
||||
/// regard as the same key.
|
||||
final Map<String, Object?> headers;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/core/models/attachment.dart';
|
||||
import 'package:stream_chat/src/core/models/reaction.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
part 'message.g.dart';
|
||||
@@ -73,6 +73,7 @@ class Message extends Equatable {
|
||||
this.extraData = const {},
|
||||
this.deletedAt,
|
||||
this.status = MessageSendingStatus.sent,
|
||||
this.i18n,
|
||||
}) : id = id ?? const Uuid().v4(),
|
||||
pinExpires = pinExpires?.toUtc(),
|
||||
createdAt = createdAt ?? DateTime.now(),
|
||||
@@ -218,6 +219,10 @@ class Message extends Equatable {
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime? deletedAt;
|
||||
|
||||
/// A Map of translations.
|
||||
@JsonKey(includeIfNull: false)
|
||||
final Map<String, String>? i18n;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serializer] methods.
|
||||
static const topLevelFields = [
|
||||
@@ -248,6 +253,7 @@ class Message extends Equatable {
|
||||
'pinned_at',
|
||||
'pin_expires',
|
||||
'pinned_by',
|
||||
'i18n',
|
||||
];
|
||||
|
||||
/// Serialize to json
|
||||
@@ -285,6 +291,7 @@ class Message extends Equatable {
|
||||
User? pinnedBy,
|
||||
Map<String, Object?>? extraData,
|
||||
MessageSendingStatus? status,
|
||||
Map<String, String>? i18n,
|
||||
}) {
|
||||
assert(() {
|
||||
if (pinExpires is! DateTime &&
|
||||
@@ -324,6 +331,7 @@ class Message extends Equatable {
|
||||
pinnedBy: pinnedBy ?? this.pinnedBy,
|
||||
pinExpires:
|
||||
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
|
||||
i18n: i18n ?? this.i18n,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -358,6 +366,7 @@ class Message extends Equatable {
|
||||
pinnedAt: other.pinnedAt,
|
||||
pinExpires: other.pinExpires,
|
||||
pinnedBy: other.pinnedBy,
|
||||
i18n: other.i18n,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -390,35 +399,6 @@ class Message extends Equatable {
|
||||
pinnedBy,
|
||||
extraData,
|
||||
status,
|
||||
i18n,
|
||||
];
|
||||
}
|
||||
|
||||
/// A translated message
|
||||
/// It has an additional property called [i18n]
|
||||
@JsonSerializable()
|
||||
class TranslatedMessage extends Message {
|
||||
/// Constructor used for json serialization
|
||||
TranslatedMessage(this.i18n) : super();
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory TranslatedMessage.fromJson(Map<String, dynamic> json) =>
|
||||
_$TranslatedMessageFromJson(
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
|
||||
);
|
||||
|
||||
/// A Map of
|
||||
final Map<String, String>? i18n;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serializer] methods.
|
||||
static final topLevelFields = [
|
||||
'i18n',
|
||||
...Message.topLevelFields,
|
||||
];
|
||||
|
||||
/// Serialize to json
|
||||
@override
|
||||
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
|
||||
_$TranslatedMessageToJson(this),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ Message _$MessageFromJson(Map<String, dynamic> json) {
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
i18n: (json['i18n'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,18 +110,6 @@ Map<String, dynamic> _$MessageToJson(Message instance) {
|
||||
val['pinned_by'] = readonly(instance.pinnedBy);
|
||||
val['extra_data'] = instance.extraData;
|
||||
writeNotNull('deleted_at', readonly(instance.deletedAt));
|
||||
writeNotNull('i18n', instance.i18n);
|
||||
return val;
|
||||
}
|
||||
|
||||
TranslatedMessage _$TranslatedMessageFromJson(Map<String, dynamic> json) {
|
||||
return TranslatedMessage(
|
||||
(json['i18n'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$TranslatedMessageToJson(TranslatedMessage instance) =>
|
||||
<String, dynamic>{
|
||||
'i18n': instance.i18n,
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ class OwnUser extends User {
|
||||
Map<String, Object?> extraData = const {},
|
||||
bool banned = false,
|
||||
List<String> teams = const [],
|
||||
String? language,
|
||||
}) : super(
|
||||
id: id,
|
||||
role: role,
|
||||
@@ -37,6 +38,7 @@ class OwnUser extends User {
|
||||
extraData: extraData,
|
||||
banned: banned,
|
||||
teams: teams,
|
||||
language: language,
|
||||
);
|
||||
|
||||
/// Create a new instance from a json
|
||||
@@ -54,6 +56,7 @@ class OwnUser extends User {
|
||||
banned: user.banned,
|
||||
extraData: user.extraData,
|
||||
teams: user.teams,
|
||||
language: user.language,
|
||||
);
|
||||
|
||||
/// Creates a copy of [OwnUser] with specified attributes overridden.
|
||||
@@ -73,6 +76,7 @@ class OwnUser extends User {
|
||||
List<Mute>? mutes,
|
||||
int? totalUnreadCount,
|
||||
int? unreadChannels,
|
||||
String? language,
|
||||
}) =>
|
||||
OwnUser(
|
||||
id: id ?? this.id,
|
||||
@@ -89,15 +93,13 @@ class OwnUser extends User {
|
||||
mutes: mutes ?? this.mutes,
|
||||
totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount,
|
||||
unreadChannels: unreadChannels ?? this.unreadChannels,
|
||||
language: language ?? this.language,
|
||||
);
|
||||
|
||||
/// Returns a new [OwnUser] that is a combination of this ownUser
|
||||
/// and the given [other] ownUser.
|
||||
OwnUser merge(OwnUser? other) {
|
||||
if (other == null) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
banned: other.banned,
|
||||
channelMutes: other.channelMutes,
|
||||
@@ -113,6 +115,7 @@ class OwnUser extends User {
|
||||
totalUnreadCount: other.totalUnreadCount,
|
||||
unreadChannels: other.unreadChannels,
|
||||
updatedAt: other.updatedAt,
|
||||
language: other.language,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,5 +39,6 @@ OwnUser _$OwnUserFromJson(Map<String, dynamic> json) {
|
||||
teams:
|
||||
(json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||
[],
|
||||
language: json['language'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class User extends Equatable {
|
||||
this.extraData = const {},
|
||||
this.banned = false,
|
||||
this.teams = const [],
|
||||
this.language,
|
||||
}) : createdAt = createdAt ?? DateTime.now(),
|
||||
updatedAt = updatedAt ?? DateTime.now();
|
||||
|
||||
@@ -36,6 +37,7 @@ class User extends Equatable {
|
||||
'online',
|
||||
'banned',
|
||||
'teams',
|
||||
'language',
|
||||
];
|
||||
|
||||
/// User id
|
||||
@@ -82,8 +84,11 @@ class User extends Equatable {
|
||||
)
|
||||
final Map<String, Object?> extraData;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
/// The language this user prefers.
|
||||
///
|
||||
/// Defaults to 'en'.
|
||||
@JsonKey(includeIfNull: false)
|
||||
final String? language;
|
||||
|
||||
/// Shortcut for user name
|
||||
String get name {
|
||||
@@ -98,11 +103,6 @@ class User extends Equatable {
|
||||
static List<String>? toIds(List<User>? users) =>
|
||||
users?.map((u) => u.id).toList();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is User && runtimeType == other.runtimeType && id == other.id;
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
|
||||
_$UserToJson(this),
|
||||
@@ -119,6 +119,7 @@ class User extends Equatable {
|
||||
Map<String, Object?>? extraData,
|
||||
bool? banned,
|
||||
List<String>? teams,
|
||||
String? language,
|
||||
}) =>
|
||||
User(
|
||||
id: id ?? this.id,
|
||||
@@ -130,18 +131,9 @@ class User extends Equatable {
|
||||
extraData: extraData ?? this.extraData,
|
||||
banned: banned ?? this.banned,
|
||||
teams: teams ?? this.teams,
|
||||
language: language ?? this.language,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
role,
|
||||
teams,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
lastActive,
|
||||
online,
|
||||
banned,
|
||||
extraData,
|
||||
];
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ User _$UserFromJson(Map<String, dynamic> json) {
|
||||
teams:
|
||||
(json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||
[],
|
||||
language: json['language'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,5 +48,6 @@ Map<String, dynamic> _$UserToJson(User instance) {
|
||||
writeNotNull('online', readonly(instance.online));
|
||||
writeNotNull('banned', readonly(instance.banned));
|
||||
val['extra_data'] = instance.extraData;
|
||||
writeNotNull('language', instance.language);
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -41,11 +41,15 @@ class WebSocket with TimerHelper {
|
||||
this.reconnectionMonitorInterval = 10,
|
||||
this.healthCheckInterval = 20,
|
||||
this.reconnectionMonitorTimeout = 40,
|
||||
this.queryParameters = const {},
|
||||
}) : _logger = logger;
|
||||
|
||||
///
|
||||
final String apiKey;
|
||||
|
||||
/// Additional query parameters to be added to the websocket url
|
||||
final Map<String, Object?> queryParameters;
|
||||
|
||||
/// WS base url
|
||||
final String baseUrl;
|
||||
|
||||
@@ -156,6 +160,7 @@ class WebSocket with TimerHelper {
|
||||
'api_key': apiKey,
|
||||
'authorization': token.rawValue,
|
||||
'stream-auth-type': token.authType.raw,
|
||||
...queryParameters,
|
||||
};
|
||||
final scheme = baseUrl.startsWith('https') ? 'wss' : 'ws';
|
||||
final host = baseUrl.replaceAll(RegExp(r'(^\w+:|^)\/\/'), '');
|
||||
|
||||
@@ -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 = '2.0.0';
|
||||
const PACKAGE_VERSION = '2.1.1';
|
||||
|
||||
@@ -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: 2.0.0
|
||||
version: 2.1.1
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e",
|
||||
"role": "test-role",
|
||||
"name": "John"
|
||||
"name": "John",
|
||||
"language": "en"
|
||||
}
|
||||
@@ -1610,9 +1610,12 @@ void main() {
|
||||
const messageId = 'test-message-id';
|
||||
const language = 'hi'; // Hindi
|
||||
const translatedMessageText = 'नमस्ते';
|
||||
final translatedMessage = TranslatedMessage(const {
|
||||
language: translatedMessageText,
|
||||
});
|
||||
|
||||
final translatedMessage = Message(
|
||||
i18n: const {
|
||||
language: translatedMessageText,
|
||||
},
|
||||
);
|
||||
|
||||
when(() => client.translateMessage(messageId, language)).thenAnswer(
|
||||
(_) async => TranslateMessageResponse()..message = translatedMessage,
|
||||
|
||||
@@ -155,7 +155,7 @@ void main() {
|
||||
|
||||
group('`.openConnection`', () {
|
||||
test('should throw if state does not contain user', () async {
|
||||
expect(client.state.user, isNull);
|
||||
expect(client.state.currentUser, isNull);
|
||||
try {
|
||||
await client.openConnection();
|
||||
} catch (e) {
|
||||
@@ -164,7 +164,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('should throw if connection is already in progress', () async {
|
||||
expect(client.state.user, isNull);
|
||||
expect(client.state.currentUser, isNull);
|
||||
try {
|
||||
await client.connectAnonymousUser();
|
||||
await client.openConnection();
|
||||
@@ -179,7 +179,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('should throw if connection is already available', () async {
|
||||
expect(client.state.user, isNull);
|
||||
expect(client.state.currentUser, isNull);
|
||||
try {
|
||||
await client.connectAnonymousUser();
|
||||
// waiting 300ms for `wsConnectionStatusStream` to emit
|
||||
@@ -799,7 +799,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('`.disconnectUser` should reset state and user', () async {
|
||||
expect(client.state.user, isNotNull);
|
||||
expect(client.state.currentUser, isNotNull);
|
||||
expect(client.wsConnectionStatus, ConnectionStatus.connected);
|
||||
|
||||
expectLater(
|
||||
@@ -810,7 +810,7 @@ void main() {
|
||||
|
||||
await client.disconnectUser();
|
||||
|
||||
expect(client.state.user, isNull);
|
||||
expect(client.state.currentUser, isNull);
|
||||
expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
|
||||
});
|
||||
});
|
||||
@@ -2109,9 +2109,11 @@ void main() {
|
||||
const messageId = 'test-message-id';
|
||||
const language = 'hi'; // Hindi
|
||||
const translatedMessageText = 'नमस्ते';
|
||||
final translatedMessage = TranslatedMessage(const {
|
||||
language: translatedMessageText,
|
||||
});
|
||||
final translatedMessage = Message(
|
||||
i18n: const {
|
||||
language: translatedMessageText,
|
||||
},
|
||||
);
|
||||
|
||||
when(() => api.message.translateMessage(messageId, language)).thenAnswer(
|
||||
(_) async => TranslateMessageResponse()..message = translatedMessage,
|
||||
|
||||
@@ -371,9 +371,11 @@ void main() {
|
||||
final path = '/messages/${message.id}/translate';
|
||||
|
||||
const translatedMessageText = 'नमस्ते';
|
||||
final translatedMessage = TranslatedMessage(const {
|
||||
language: translatedMessageText,
|
||||
});
|
||||
final translatedMessage = Message(
|
||||
i18n: const {
|
||||
language: translatedMessageText,
|
||||
},
|
||||
);
|
||||
|
||||
when(() => client.post(
|
||||
path,
|
||||
|
||||
@@ -9,6 +9,8 @@ void main() {
|
||||
expect(options.baseUrl, 'https://chat-us-east-1.stream-io-api.com');
|
||||
expect(options.connectTimeout, const Duration(seconds: 6));
|
||||
expect(options.receiveTimeout, const Duration(seconds: 6));
|
||||
expect(options.queryParameters, const {});
|
||||
expect(options.headers, const {});
|
||||
});
|
||||
|
||||
test('should override all the default set params', () {
|
||||
@@ -16,11 +18,15 @@ void main() {
|
||||
baseUrl: 'base-url',
|
||||
connectTimeout: Duration(seconds: 3),
|
||||
receiveTimeout: Duration(seconds: 3),
|
||||
headers: {'test': 'test'},
|
||||
queryParameters: {'123': '123'},
|
||||
);
|
||||
expect(options.location, isNull);
|
||||
expect(options.baseUrl, 'base-url');
|
||||
expect(options.connectTimeout, const Duration(seconds: 3));
|
||||
expect(options.receiveTimeout, const Duration(seconds: 3));
|
||||
expect(options.headers, {'test': 'test'});
|
||||
expect(options.queryParameters, {'123': '123'});
|
||||
});
|
||||
|
||||
group('should create baseUrl according to provided location', () {
|
||||
|
||||
@@ -96,7 +96,7 @@ void main() {
|
||||
await client.get('path');
|
||||
} catch (_) {}
|
||||
|
||||
verify(() => logger.info(any())).called(16);
|
||||
verify(() => logger.info(any())).called(greaterThan(0));
|
||||
});
|
||||
|
||||
test('loggingInterceptor should log error', () async {
|
||||
@@ -108,7 +108,7 @@ void main() {
|
||||
await client.get('path');
|
||||
} catch (_) {}
|
||||
|
||||
verify(() => logger.severe(any())).called(8);
|
||||
verify(() => logger.severe(any())).called(greaterThan(0));
|
||||
});
|
||||
|
||||
test('`.lock` should lock the dio client', () async {
|
||||
|
||||
@@ -28,6 +28,7 @@ void main() {
|
||||
expect(message.pinnedAt, null);
|
||||
expect(message.pinExpires, null);
|
||||
expect(message.pinnedBy, null);
|
||||
expect(message.i18n, null);
|
||||
});
|
||||
|
||||
test('should serialize to json correctly', () {
|
||||
|
||||
@@ -89,7 +89,7 @@ class FakeChatApi extends Fake implements StreamChatApi {
|
||||
|
||||
class FakeClientState extends Fake implements ClientState {
|
||||
@override
|
||||
OwnUser? get user => OwnUser(id: 'test-user-id');
|
||||
OwnUser? get currentUser => OwnUser(id: 'test-user-id');
|
||||
|
||||
@override
|
||||
int totalUnreadCount = 0;
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
## Upcoming
|
||||
## 2.1.1
|
||||
|
||||
✅ Added
|
||||
- Updated core dependency
|
||||
|
||||
## 2.1.0
|
||||
|
||||
✅ Added
|
||||
|
||||
- Added `MessageListView.paginationLimit`
|
||||
- `MessageText` renders message translation if available
|
||||
- Allow the various ListView widgets to be themed via ThemeData classes
|
||||
- Added `bottomRowBuilder` and `deletedBottomRowBuilder` that build a widget below a `MessageWidget`
|
||||
|
||||
🔄 Changed
|
||||
|
||||
- `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`.
|
||||
- `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Fix floating date divider not having a fixed size
|
||||
|
||||
## 2.0.0
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
- [Register](https://getstream.io/chat/trial/) to get an API key for Stream Chat
|
||||
- [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/)
|
||||
- [Chat UI Kit](https://getstream.io/chat/ui-kit/)
|
||||
|
||||
- [UI Docs](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/introduction/)
|
||||
- [Chat Client Docs](https://getstream.io/chat/docs/flutter-dart/?language=dart)
|
||||
|
||||
### Changelog
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
@@ -41,7 +41,7 @@ android {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.example.example"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.5.20'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.2'
|
||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
|
||||
|
||||
/// A chat-persisted StreamChatClient
|
||||
final chatPersistentClient = StreamChatPersistenceClient(
|
||||
logLevel: Level.INFO,
|
||||
);
|
||||
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -15,7 +12,7 @@ void main() async {
|
||||
final client = StreamChatClient(
|
||||
'kv7mcsxr24p8',
|
||||
logLevel: Level.INFO,
|
||||
)..chatPersistenceClient = chatPersistentClient;
|
||||
);
|
||||
|
||||
/// Set the current user and connect the websocket. In a production
|
||||
/// scenario, this should be done using a backend to generate a user token
|
||||
@@ -76,6 +73,13 @@ class MyApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('hi'),
|
||||
Locale('fr'),
|
||||
Locale('it'),
|
||||
],
|
||||
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
|
||||
builder: (context, widget) => StreamChat(
|
||||
client: client,
|
||||
child: widget,
|
||||
|
||||
@@ -103,7 +103,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
: null,
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).user!.id],
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
pagination: const PaginationParams(
|
||||
|
||||
@@ -80,7 +80,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
child: ChannelListView(
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).user!.id],
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
pagination: const PaginationParams(
|
||||
|
||||
@@ -81,7 +81,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
child: ChannelListView(
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).user!.id],
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
channelPreviewBuilder: _channelPreviewBuilder,
|
||||
// sort: [SortOption('last_message_at')],
|
||||
|
||||
@@ -66,7 +66,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
child: ChannelListView(
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).user!.id],
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
pagination: const PaginationParams(
|
||||
|
||||
@@ -72,7 +72,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
child: ChannelListView(
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).user!.id],
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
pagination: const PaginationParams(
|
||||
@@ -115,7 +115,8 @@ class ChannelPage extends StatelessWidget {
|
||||
MessageWidget _,
|
||||
) {
|
||||
final message = details.message;
|
||||
final isCurrentUser = StreamChat.of(context).user!.id == message.user!.id;
|
||||
final isCurrentUser =
|
||||
StreamChat.of(context).currentUser!.id == message.user!.id;
|
||||
final textAlign = isCurrentUser ? TextAlign.right : TextAlign.left;
|
||||
final color = isCurrentUser ? Colors.blueGrey : Colors.blue;
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
child: ChannelListView(
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).user!.id],
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
pagination: const PaginationParams(
|
||||
|
||||
@@ -33,6 +33,8 @@ dependencies:
|
||||
# path: ../../stream_chat_flutter_core
|
||||
stream_chat_flutter:
|
||||
path: ../
|
||||
stream_chat_localizations:
|
||||
path: ../../stream_chat_localizations
|
||||
stream_chat_persistence:
|
||||
path: ../../stream_chat_persistence
|
||||
|
||||
|
||||
+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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export 'package:jiffy/jiffy.dart';
|
||||
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
export 'src/attachment/attachment.dart';
|
||||
@@ -14,7 +15,10 @@ export 'src/deleted_message.dart';
|
||||
export 'src/full_screen_media.dart';
|
||||
export 'src/gallery_footer.dart';
|
||||
export 'src/gallery_header.dart';
|
||||
export 'src/gradient_avatar.dart';
|
||||
export 'src/info_tile.dart';
|
||||
export 'src/localization/stream_chat_localizations.dart';
|
||||
export 'src/localization/translations.dart' show DefaultTranslations;
|
||||
export 'src/mention_tile.dart';
|
||||
export 'src/message_action.dart';
|
||||
export 'src/message_input.dart';
|
||||
@@ -41,3 +45,4 @@ export 'src/user_item.dart';
|
||||
export 'src/user_list_view.dart';
|
||||
export 'src/user_list_view.dart';
|
||||
export 'src/utils.dart';
|
||||
export 'src/visible_footnote.dart';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path transform="translate(24, 24) rotate(-180)" d="M5.30605 8.30597C5.11052 8.50222 5.00073 8.76795 5.00073 9.04498C5.00073 9.322 5.11052 9.58774 5.30605 9.78398L11.2061 15.697C11.4321 15.923 11.7351 16.024 12.0291 16C12.17 16.0032 12.3102 15.9778 12.441 15.9252C12.5718 15.8726 12.6906 15.7939 12.7901 15.694L18.6941 9.78398C18.8898 9.58786 18.9998 9.32208 18.9998 9.04498C18.9998 8.76787 18.8898 8.50209 18.6941 8.30597C18.5972 8.20898 18.4821 8.13203 18.3555 8.07952C18.2289 8.02702 18.0931 8 17.9561 8C17.819 8 17.6832 8.02702 17.5566 8.07952C17.43 8.13203 17.3149 8.20898 17.2181 8.30597L11.9981 13.533L6.78005 8.30597C6.68327 8.20901 6.56831 8.13208 6.44176 8.07959C6.31521 8.0271 6.17956 8.00008 6.04255 8.00008C5.90555 8.00008 5.76989 8.0271 5.64334 8.07959C5.51679 8.13208 5.40184 8.20901 5.30505 8.30597H5.30605Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 943 B |
@@ -1,12 +1,13 @@
|
||||
name: stream_chat_flutter
|
||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
|
||||
version: 2.0.0
|
||||
version: 2.1.1
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
cached_network_image: ^3.0.0
|
||||
@@ -30,13 +31,13 @@ dependencies:
|
||||
lottie: ^1.0.1
|
||||
meta: ^1.3.0
|
||||
path_provider: ^2.0.1
|
||||
photo_manager: ^1.1.6
|
||||
photo_manager: ^1.2.6+1
|
||||
photo_view: ^0.12.0
|
||||
rxdart: ^0.27.0
|
||||
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
||||
share_plus: ^2.0.3
|
||||
shimmer: ^2.0.0
|
||||
stream_chat_flutter_core: ^2.0.0
|
||||
stream_chat_flutter_core: ^2.1.1
|
||||
substring_highlight: ^1.0.26
|
||||
synchronized: ^3.0.0
|
||||
url_launcher: ^6.0.3
|
||||
@@ -58,5 +59,6 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
golden_toolkit: ^0.9.0
|
||||
mocktail: ^0.1.2
|
||||
path: ^1.8.0
|
||||
pedantic: ^1.11.0
|
||||
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
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';
|
||||
|
||||
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
|
||||
await loadAppFonts();
|
||||
goldenFileComparator =
|
||||
CustomGoldenFileComparator(Uri.parse('test/src/goldens'));
|
||||
return testMain();
|
||||
}
|
||||
|
||||
class CustomGoldenFileComparator extends LocalFileComparator {
|
||||
CustomGoldenFileComparator(Uri testFile) : super(testFile);
|
||||
|
||||
@override
|
||||
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
|
||||
final result = await GoldenFileComparator.compareLists(
|
||||
imageBytes,
|
||||
await getGoldenBytes(golden),
|
||||
);
|
||||
|
||||
if (!result.passed && result.diffPercent > 0.05) {
|
||||
final error = await generateFailureOutput(result, golden, basedir);
|
||||
throw FlutterError(error);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(Uri golden, Uint8List imageBytes) =>
|
||||
super.update(golden, imageBytes);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
|
||||
final themeData = ThemeData();
|
||||
final streamTheme = StreamChatThemeData.fromTheme(themeData);
|
||||
@@ -77,7 +77,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id2'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id2'));
|
||||
|
||||
final themeData = ThemeData();
|
||||
final streamTheme = StreamChatThemeData.fromTheme(themeData);
|
||||
@@ -119,7 +119,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
|
||||
final themeData = ThemeData();
|
||||
final streamTheme = StreamChatThemeData.fromTheme(themeData);
|
||||
@@ -160,7 +160,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
|
||||
final themeData = ThemeData();
|
||||
final streamTheme = StreamChatThemeData.fromTheme(themeData);
|
||||
@@ -207,7 +207,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
|
||||
final themeData = ThemeData();
|
||||
final streamTheme = StreamChatThemeData.fromTheme(themeData);
|
||||
@@ -254,7 +254,7 @@ void main() {
|
||||
when(() => mockChannel.updateMessage(any()))
|
||||
.thenAnswer((_) async => UpdateMessageResponse());
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
|
||||
final message = Message(
|
||||
text: 'test',
|
||||
@@ -308,7 +308,7 @@ void main() {
|
||||
when(() => mockChannel.updateMessage(any()))
|
||||
.thenAnswer((_) async => UpdateMessageResponse());
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
|
||||
final message = Message(
|
||||
text: 'test',
|
||||
@@ -357,7 +357,7 @@ void main() {
|
||||
when(() => mockChannel.deleteMessage(any()))
|
||||
.thenAnswer((_) async => EmptyResponse());
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
|
||||
final message = Message(
|
||||
user: User(
|
||||
@@ -399,7 +399,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
|
||||
final imageDownloader = MockAttachmentDownloader();
|
||||
|
||||
@@ -454,7 +454,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
|
||||
final fileDownloader = MockAttachmentDownloader();
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@ void main() {
|
||||
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(user);
|
||||
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
|
||||
when(() => clientState.currentUser).thenReturn(user);
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(user));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
@@ -82,8 +83,9 @@ void main() {
|
||||
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(user);
|
||||
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
|
||||
when(() => clientState.currentUser).thenReturn(user);
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(user));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
@@ -149,8 +151,9 @@ void main() {
|
||||
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(user);
|
||||
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
|
||||
when(() => clientState.currentUser).thenReturn(user);
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(user));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
@@ -217,8 +220,9 @@ void main() {
|
||||
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(user);
|
||||
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
|
||||
when(() => clientState.currentUser).thenReturn(user);
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(user));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
@@ -293,8 +297,9 @@ void main() {
|
||||
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(user);
|
||||
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
|
||||
when(() => clientState.currentUser).thenReturn(user);
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(user));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
@@ -360,8 +365,9 @@ void main() {
|
||||
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(user);
|
||||
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
|
||||
when(() => clientState.currentUser).thenReturn(user);
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(user));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
|
||||
@@ -17,7 +17,7 @@ void main() {
|
||||
final channelState = MockChannelState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
@@ -56,7 +56,7 @@ void main() {
|
||||
final channelState = MockChannelState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
@@ -135,7 +135,7 @@ void main() {
|
||||
final currentUser = OwnUser(id: 'user-id');
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(currentUser);
|
||||
when(() => clientState.currentUser).thenReturn(currentUser);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
@@ -207,7 +207,7 @@ void main() {
|
||||
final channelState = MockChannelState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({
|
||||
|
||||
@@ -13,7 +13,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => client.wsConnectionStatusStream)
|
||||
.thenAnswer((_) => Stream.value(ConnectionStatus.connected));
|
||||
|
||||
@@ -30,7 +30,7 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final userAvatar = tester.widget<UserAvatar>(find.byType(UserAvatar));
|
||||
expect(userAvatar.user, clientState.user);
|
||||
expect(userAvatar.user, clientState.currentUser);
|
||||
expect(find.byType(StreamNeumorphicButton), findsOneWidget);
|
||||
expect(find.text('Stream Chat'), findsOneWidget);
|
||||
},
|
||||
@@ -43,7 +43,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => client.wsConnectionStatusStream)
|
||||
.thenAnswer((_) => Stream.value(ConnectionStatus.disconnected));
|
||||
|
||||
@@ -72,7 +72,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => client.wsConnectionStatusStream)
|
||||
.thenAnswer((_) => Stream.value(ConnectionStatus.connecting));
|
||||
|
||||
@@ -101,7 +101,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => client.wsConnectionStatusStream)
|
||||
.thenAnswer((_) => Stream.value(ConnectionStatus.connecting));
|
||||
|
||||
@@ -139,7 +139,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => client.wsConnectionStatusStream)
|
||||
.thenAnswer((_) => Stream.value(ConnectionStatus.connecting));
|
||||
|
||||
@@ -173,7 +173,7 @@ void main() {
|
||||
final clientState = MockClientState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => client.wsConnectionStatusStream)
|
||||
.thenAnswer((_) => Stream.value(ConnectionStatus.connecting));
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ void main() {
|
||||
final lastMessageAt = DateTime.parse('2020-06-22 12:00:00');
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
|
||||
@@ -18,8 +18,9 @@ void main() {
|
||||
|
||||
when(() => channel.cid).thenReturn('cid');
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.user).thenReturn(user);
|
||||
when(() => clientState.userStream).thenAnswer((_) => Stream.value(user));
|
||||
when(() => clientState.currentUser).thenReturn(user);
|
||||
when(() => clientState.currentUserStream)
|
||||
.thenAnswer((_) => Stream.value(user));
|
||||
when(() => channel.lastMessageAt).thenReturn(lastMessageAt);
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
when(() => channel.client).thenReturn(client);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user