Merge branch 'develop' into feat/open-library-from-limited-access

This commit is contained in:
Salvatore Giordano
2022-04-28 10:12:47 +02:00
committed by GitHub
24 changed files with 304 additions and 120 deletions
@@ -64,30 +64,34 @@ The second type looks like this:
We can use a `Stack` for achieving this: We can use a `Stack` for achieving this:
```dart ```dart
Scaffold( Stack(
body: Stack( children: <Widget>[
children: <Widget>[ // Add your video implementation here
// Add your video implementation here ShaderMask(
ShaderMask( shaderCallback: (rect) {
shaderCallback: (rect) { return const LinearGradient(
return LinearGradient(
begin: Alignment.bottomCenter, begin: Alignment.bottomCenter,
end: Alignment.topCenter, end: Alignment.topCenter,
colors: [Colors.black, Colors.transparent], colors: [Colors.black, Colors.transparent],
stops: [0.4, 0.65] stops: [0.4, 0.8]).createShader(
).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); Rect.fromLTRB(0, 0, rect.width, rect.height),
}, );
blendMode: BlendMode.dstIn, },
child: Column( blendMode: BlendMode.dstIn,
children: [ child: Column(
Expanded( children: const [
child: MessageListView(), Expanded(
), child: MessageListViewTheme(
MessageInput(), data: MessageListViewThemeData(
], backgroundColor: Colors.transparent,
), ),
), child: MessageListView(),
], ),
), ),
) MessageInput(),
``` ],
),
),
],
),
```
@@ -86,6 +86,14 @@ firebaseMessaging.onTokenRefresh.listen((token) {
}); });
``` ```
Push Notifications v2 also supports specifying a name to the push device tokens you register. By setting the optional `pushProviderName` param in the `addDevice` call you can support different configurations between the device and the `PushProvider`.
```dart
firebaseMessaging.onTokenRefresh.listen((token) {
client.addDevice(token, PushProvider.firebase, pushProviderName: 'my-custom-config');
});
```
### Receiving Notifications ### Receiving Notifications
Push notifications behave a bit differently depending on whether you are using iOS or Android. Push notifications behave a bit differently depending on whether you are using iOS or Android.
+6
View File
@@ -1,3 +1,9 @@
## Upcoming
✅ Added
- Added `push_provider_name` to `addDevice` API call
## 3.6.1 ## 3.6.1
🐞 Fixed 🐞 Fixed
+24 -16
View File
@@ -64,7 +64,7 @@ class StreamChatClient {
StreamChatClient( StreamChatClient(
String apiKey, { String apiKey, {
this.logLevel = Level.WARNING, this.logLevel = Level.WARNING,
LogHandlerFunction? logHandlerFunction, this.logHandlerFunction = StreamChatClient.defaultLogHandler,
RetryPolicy? retryPolicy, RetryPolicy? retryPolicy,
@Deprecated(''' @Deprecated('''
Location is now deprecated in favor of the new edge server. Will be removed in v4.0.0. Location is now deprecated in favor of the new edge server. Will be removed in v4.0.0.
@@ -77,7 +77,6 @@ class StreamChatClient {
WebSocket? ws, WebSocket? ws,
AttachmentFileUploader? attachmentFileUploader, AttachmentFileUploader? attachmentFileUploader,
}) { }) {
this.logHandlerFunction = logHandlerFunction ?? _defaultLogHandler;
logger.info('Initiating new StreamChatClient'); logger.info('Initiating new StreamChatClient');
final options = StreamHttpClientOptions( final options = StreamHttpClientOptions(
@@ -134,7 +133,7 @@ class StreamChatClient {
'${CurrentPlatform.name}-' '${CurrentPlatform.name}-'
'${PACKAGE_VERSION.split('+')[0]}'; '${PACKAGE_VERSION.split('+')[0]}';
/// Additionals headers for all requests /// Additional headers for all requests
static Map<String, Object?> additionalHeaders = {}; static Map<String, Object?> additionalHeaders = {};
ChatPersistenceClient? _originalChatPersistenceClient; ChatPersistenceClient? _originalChatPersistenceClient;
@@ -189,7 +188,7 @@ class StreamChatClient {
/// final client = StreamChatClient("stream-chat-api-key", /// final client = StreamChatClient("stream-chat-api-key",
/// logHandlerFunction: myLogHandlerFunction); /// logHandlerFunction: myLogHandlerFunction);
///``` ///```
late LogHandlerFunction logHandlerFunction; final LogHandlerFunction logHandlerFunction;
StreamSubscription<ConnectionStatus>? _connectionStatusSubscription; StreamSubscription<ConnectionStatus>? _connectionStatusSubscription;
@@ -214,17 +213,18 @@ class StreamChatClient {
Stream<ConnectionStatus> get wsConnectionStatusStream => Stream<ConnectionStatus> get wsConnectionStatusStream =>
_wsConnectionStatusController.stream.distinct(); _wsConnectionStatusController.stream.distinct();
LogHandlerFunction get _defaultLogHandler => (LogRecord record) { /// Default log handler function for the [StreamChatClient] logger.
print( static void defaultLogHandler(LogRecord record) {
'${record.time} ' print(
'${_levelEmojiMapper[record.level] ?? record.level.name} ' '${record.time} '
'${record.loggerName} ${record.message} ', '${_levelEmojiMapper[record.level] ?? record.level.name} '
); '${record.loggerName} ${record.message} ',
if (record.error != null) print(record.error); );
if (record.stackTrace != null) print(record.stackTrace); if (record.error != null) print(record.error);
}; if (record.stackTrace != null) print(record.stackTrace);
}
/// /// Default logger for the [StreamChatClient].
Logger detachedLogger(String name) => Logger.detached(name) Logger detachedLogger(String name) => Logger.detached(name)
..level = logLevel ..level = logLevel
..onRecord.listen(logHandlerFunction); ..onRecord.listen(logHandlerFunction);
@@ -818,8 +818,16 @@ class StreamChatClient {
); );
/// Add a device for Push Notifications. /// Add a device for Push Notifications.
Future<EmptyResponse> addDevice(String id, PushProvider pushProvider) => Future<EmptyResponse> addDevice(
_chatApi.device.addDevice(id, pushProvider); String id,
PushProvider pushProvider, {
String? pushProviderName,
}) =>
_chatApi.device.addDevice(
id,
pushProvider,
pushProviderName: pushProviderName,
);
/// Gets a list of user devices. /// Gets a list of user devices.
Future<ListDevicesResponse> getDevices() => _chatApi.device.getDevices(); Future<ListDevicesResponse> getDevices() => _chatApi.device.getDevices();
@@ -29,13 +29,16 @@ class DeviceApi {
/// Add a device for Push Notifications. /// Add a device for Push Notifications.
Future<EmptyResponse> addDevice( Future<EmptyResponse> addDevice(
String deviceId, String deviceId,
PushProvider pushProvider, PushProvider pushProvider, {
) async { String? pushProviderName,
}) async {
final response = await _client.post( final response = await _client.post(
'/devices', '/devices',
data: { data: {
'id': deviceId, 'id': deviceId,
'push_provider': pushProvider.name, 'push_provider': pushProvider.name,
if (pushProviderName != null && pushProviderName.isNotEmpty)
'push_provider_name': pushProviderName,
}, },
); );
return EmptyResponse.fromJson(response.data); return EmptyResponse.fromJson(response.data);
+29 -30
View File
@@ -5,37 +5,36 @@ export 'package:dio/src/dio_error.dart';
export 'package:dio/src/multipart_file.dart'; export 'package:dio/src/multipart_file.dart';
export 'package:dio/src/options.dart'; export 'package:dio/src/options.dart';
export 'package:dio/src/options.dart' show ProgressCallback; export 'package:dio/src/options.dart' show ProgressCallback;
export 'package:logging/logging.dart' show Logger, Level; export 'package:logging/logging.dart' show Logger, Level, LogRecord;
export 'package:rate_limiter/rate_limiter.dart'; export 'package:rate_limiter/rate_limiter.dart';
export './src/core/api/attachment_file_uploader.dart'
show AttachmentFileUploader;
export './src/core/api/requests.dart';
export './src/core/api/requests.dart';
export './src/core/api/responses.dart';
export './src/core/api/stream_chat_api.dart' show PushProvider;
export './src/core/error/error.dart';
export './src/core/models/action.dart';
export './src/core/models/attachment.dart';
export './src/core/models/attachment_file.dart';
export './src/core/models/channel_config.dart';
export './src/core/models/channel_model.dart';
export './src/core/models/channel_state.dart';
export './src/core/models/command.dart';
export './src/core/models/device.dart';
export './src/core/models/event.dart';
export './src/core/models/filter.dart' show Filter;
export './src/core/models/member.dart';
export './src/core/models/message.dart';
export './src/core/models/mute.dart';
export './src/core/models/own_user.dart';
export './src/core/models/reaction.dart';
export './src/core/models/read.dart';
export './src/core/models/user.dart';
export './src/core/util/extension.dart';
export './src/db/chat_persistence_client.dart';
export './src/event_type.dart';
export './src/location.dart';
export './src/ws/connection_status.dart';
export 'src/client/channel.dart'; export 'src/client/channel.dart';
export 'src/client/client.dart'; export 'src/client/client.dart';
export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader;
export 'src/core/api/requests.dart';
export 'src/core/api/requests.dart';
export 'src/core/api/responses.dart';
export 'src/core/api/stream_chat_api.dart' show PushProvider;
export 'src/core/error/error.dart';
export 'src/core/models/action.dart';
export 'src/core/models/attachment.dart';
export 'src/core/models/attachment_file.dart';
export 'src/core/models/channel_config.dart';
export 'src/core/models/channel_model.dart';
export 'src/core/models/channel_state.dart';
export 'src/core/models/command.dart';
export 'src/core/models/device.dart';
export 'src/core/models/event.dart';
export 'src/core/models/filter.dart' show Filter;
export 'src/core/models/member.dart';
export 'src/core/models/message.dart';
export 'src/core/models/mute.dart';
export 'src/core/models/own_user.dart';
export 'src/core/models/reaction.dart';
export 'src/core/models/read.dart';
export 'src/core/models/user.dart';
export 'src/core/util/extension.dart';
export 'src/db/chat_persistence_client.dart';
export 'src/event_type.dart';
export 'src/location.dart';
export 'src/ws/connection_status.dart';
@@ -1171,7 +1171,7 @@ void main() {
verifyNoMoreInteractions(api.channel); verifyNoMoreInteractions(api.channel);
}); });
test('`.addDevice`', () async { test('`.addDevice should work`', () async {
const id = 'test-device-id'; const id = 'test-device-id';
const provider = PushProvider.firebase; const provider = PushProvider.firebase;
@@ -1185,6 +1185,34 @@ void main() {
verifyNoMoreInteractions(api.device); verifyNoMoreInteractions(api.device);
}); });
test('`.addDevice should work with pushProviderName`', () async {
const id = 'test-device-id';
const provider = PushProvider.firebase;
const pushProviderName = 'my-custom-config';
when(
() => api.device.addDevice(
id,
provider,
pushProviderName: pushProviderName,
),
).thenAnswer((_) async => EmptyResponse());
final res = await client.addDevice(
id,
provider,
pushProviderName: pushProviderName,
);
expect(res, isNotNull);
verify(() => api.device.addDevice(
id,
provider,
pushProviderName: pushProviderName,
)).called(1);
verifyNoMoreInteractions(api.device);
});
test('`.getDevices`', () async { test('`.getDevices`', () async {
final devices = List.generate( final devices = List.generate(
3, 3,
@@ -20,7 +20,7 @@ void main() {
deviceApi = DeviceApi(client); deviceApi = DeviceApi(client);
}); });
test('addDevice', () async { test('addDevice should work', () async {
const deviceId = 'test-device-id'; const deviceId = 'test-device-id';
const pushProvider = PushProvider.firebase; const pushProvider = PushProvider.firebase;
@@ -44,6 +44,36 @@ void main() {
verifyNoMoreInteractions(client); verifyNoMoreInteractions(client);
}); });
test('addDevice should work with pushProviderName', () async {
const deviceId = 'test-device-id';
const pushProvider = PushProvider.firebase;
const pushProviderName = 'my-custom-config';
const path = '/devices';
when(() => client.post(
path,
data: {
'id': deviceId,
'push_provider': pushProvider.name,
'push_provider_name': pushProviderName,
},
))
.thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{}));
final res = await deviceApi.addDevice(
deviceId,
pushProvider,
pushProviderName: pushProviderName,
);
expect(res, isNotNull);
verify(() => client.post(path, data: any(named: 'data'))).called(1);
verifyNoMoreInteractions(client);
});
test('getDevices', () async { test('getDevices', () async {
const path = '/devices'; const path = '/devices';
+9 -1
View File
@@ -3,6 +3,14 @@
✅ Added ✅ Added
- [[#1087]](https://github.com/GetStream/stream-chat-flutter/issues/1087): Handle limited access to camera on iOS. - [[#1087]](https://github.com/GetStream/stream-chat-flutter/issues/1087): Handle limited access to camera on iOS.
- `centerTitle` and `elevation` properties to `ChannelHeader`, `ThreadHeader` and `ChannelListHeader`.
🐞 Fixed
- [[#1067]](https://github.com/GetStream/stream-chat-flutter/issues/1067): Fix name text overflow in reaction card.
- [[#842]](https://github.com/GetStream/stream-chat-flutter/issues/842): show date divider for first message.
- Loosen up url check for attachment download.
- Use `ogScrapeUrl` for LinkAttachments.
## 3.6.1 ## 3.6.1
@@ -12,7 +20,7 @@
🐞 Fixed 🐞 Fixed
-[[#892]](https://github.com/GetStream/stream-chat-flutter/issues/892): Fix default `initialAlignment` in `MessageListView`. - [[#892]](https://github.com/GetStream/stream-chat-flutter/issues/892): Fix default `initialAlignment` in `MessageListView`.
- Fix `MessageInputTheme.inputBackgroundColor` color not being used in some widgets of `MessageInput` - Fix `MessageInputTheme.inputBackgroundColor` color not being used in some widgets of `MessageInput`
- Removed dependency on `visibility_detector` - Removed dependency on `visibility_detector`
@@ -18,14 +18,11 @@ class AttachmentTitle extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final normalizedTitleLink = attachment.titleLink?.replaceFirst( final ogScrapeUrl = attachment.ogScrapeUrl;
RegExp(r'https?://(www\.)?'),
'',
);
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
final titleLink = attachment.titleLink; final ogScrapeUrl = attachment.ogScrapeUrl;
if (titleLink != null) launchURL(context, titleLink); if (ogScrapeUrl != null) launchURL(context, ogScrapeUrl);
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
@@ -42,8 +39,8 @@ class AttachmentTitle extends StatelessWidget {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
if (normalizedTitleLink != null) if (ogScrapeUrl != null)
Text(normalizedTitleLink, style: messageTheme.messageTextStyle), Text(ogScrapeUrl, style: messageTheme.messageTextStyle),
], ],
), ),
), ),
@@ -37,11 +37,11 @@ class UrlAttachment extends StatelessWidget {
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
final titleLink = urlAttachment.titleLink; final ogScrapeUrl = urlAttachment.ogScrapeUrl;
if (titleLink != null) { if (ogScrapeUrl != null) {
onLinkTap != null onLinkTap != null
? onLinkTap!(titleLink) ? onLinkTap!(ogScrapeUrl)
: launchURL(context, titleLink); : launchURL(context, ogScrapeUrl);
} }
}, },
child: Column( child: Column(
@@ -61,9 +61,11 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
this.showConnectionStateTile = false, this.showConnectionStateTile = false,
this.title, this.title,
this.subtitle, this.subtitle,
this.centerTitle,
this.leading, this.leading,
this.actions, this.actions,
this.backgroundColor, this.backgroundColor,
this.elevation = 1,
}) : preferredSize = const Size.fromHeight(kToolbarHeight), }) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key); super(key: key);
@@ -92,6 +94,9 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// Subtitle widget /// Subtitle widget
final Widget? subtitle; final Widget? subtitle;
/// Whether the title should be centered
final bool? centerTitle;
/// Leading widget /// Leading widget
final Widget? leading; final Widget? leading;
@@ -102,8 +107,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// The background color for this [ChannelHeader]. /// The background color for this [ChannelHeader].
final Color? backgroundColor; final Color? backgroundColor;
/// The elevation for this [ChannelHeader].
final double elevation;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final effectiveCenterTitle = getEffectiveCenterTitle(
Theme.of(context),
actions: actions,
centerTitle: centerTitle,
);
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
final channelHeaderTheme = ChannelHeaderTheme.of(context); final channelHeaderTheme = ChannelHeaderTheme.of(context);
@@ -144,7 +157,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
systemOverlayStyle: theme.brightness == Brightness.dark systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light ? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark, : SystemUiOverlayStyle.dark,
elevation: 1, elevation: elevation,
leading: leadingWidget, leading: leadingWidget,
backgroundColor: backgroundColor ?? channelHeaderTheme.color, backgroundColor: backgroundColor ?? channelHeaderTheme.color,
actions: actions ?? actions: actions ??
@@ -162,14 +175,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
), ),
], ],
centerTitle: true, centerTitle: centerTitle,
title: InkWell( title: InkWell(
onTap: onTitleTap, onTap: onTitleTap,
child: SizedBox( child: SizedBox(
height: preferredSize.height, height: preferredSize.height,
width: preferredSize.width,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: effectiveCenterTitle
? CrossAxisAlignment.center
: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
title ?? title ??
ChannelName( ChannelName(
@@ -56,9 +56,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
this.showConnectionStateTile = false, this.showConnectionStateTile = false,
this.preNavigationCallback, this.preNavigationCallback,
this.subtitle, this.subtitle,
this.centerTitle,
this.leading, this.leading,
this.actions, this.actions,
this.backgroundColor, this.backgroundColor,
this.elevation = 1,
}) : super(key: key); }) : super(key: key);
/// Pass this if you don't have a [StreamChatClient] in your widget tree. /// Pass this if you don't have a [StreamChatClient] in your widget tree.
@@ -83,6 +85,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
/// Subtitle widget /// Subtitle widget
final Widget? subtitle; final Widget? subtitle;
/// Whether the title should be centered
final bool? centerTitle;
/// Leading widget /// Leading widget
/// By default it shows the logged in user avatar /// By default it shows the logged in user avatar
final Widget? leading; final Widget? leading;
@@ -94,6 +99,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
/// The background color for this [ChannelListHeader]. /// The background color for this [ChannelListHeader].
final Color? backgroundColor; final Color? backgroundColor;
/// The elevation for this [ChannelListHeader].
final double elevation;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _client = client ?? StreamChat.of(context).client; final _client = client ?? StreamChat.of(context).client;
@@ -128,10 +136,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
systemOverlayStyle: theme.brightness == Brightness.dark systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light ? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark, : SystemUiOverlayStyle.dark,
elevation: 1, elevation: elevation,
backgroundColor: backgroundColor:
backgroundColor ?? channelListHeaderThemeData.color, backgroundColor ?? channelListHeaderThemeData.color,
centerTitle: true, centerTitle: centerTitle,
leading: leading ?? leading: leading ??
Center( Center(
child: user != null child: user != null
@@ -225,3 +225,12 @@ extension UserListX on List<User> {
return entries.map((e) => e.key).toList(growable: false); return entries.map((e) => e.key).toList(growable: false);
} }
} }
/// Extensions on [Uri]
extension UriX on Uri {
/// Return the URI adding the http scheme if it is missing
Uri get withScheme {
if (hasScheme) return this;
return Uri.parse('http://${toString()}');
}
}
@@ -1297,7 +1297,7 @@ class MessageInputState extends State<MessageInput> {
Widget _buildReplyToMessage() { Widget _buildReplyToMessage() {
if (!_hasQuotedMessage) return const Offstage(); if (!_hasQuotedMessage) return const Offstage();
final containsUrl = widget.quotedMessage!.attachments final containsUrl = widget.quotedMessage!.attachments
.any((element) => element.titleLink != null); .any((element) => element.ogScrapeUrl != null);
return QuotedMessageWidget( return QuotedMessageWidget(
reverse: true, reverse: true,
showBorder: !containsUrl, showBorder: !containsUrl,
@@ -528,6 +528,7 @@ class _MessageListViewState extends State<MessageListView> {
return ((index + 2) * 2) - 1; return ((index + 2) * 2) - 1;
} }
} }
return null;
}, },
// Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages) // Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages)
@@ -559,6 +560,9 @@ class _MessageListViewState extends State<MessageListView> {
if (widget.reverse if (widget.reverse
? widget.headerBuilder == null ? widget.headerBuilder == null
: widget.footerBuilder == null) { : widget.footerBuilder == null) {
if (messages.isNotEmpty) {
return _buildDateDivider(messages.last);
}
if (_isThreadConversation) return const Offstage(); if (_isThreadConversation) return const Offstage();
return const SizedBox(height: 52); return const SizedBox(height: 52);
} }
@@ -583,21 +587,12 @@ class _MessageListViewState extends State<MessageListView> {
message = messages[i - 2]; message = messages[i - 2];
nextMessage = messages[i - 1]; nextMessage = messages[i - 1];
} }
if (!Jiffy(message.createdAt.toLocal()).isSame( if (!Jiffy(message.createdAt.toLocal()).isSame(
nextMessage.createdAt.toLocal(), nextMessage.createdAt.toLocal(),
Units.DAY, Units.DAY,
)) { )) {
final divider = widget.dateDividerBuilder != null return _buildDateDivider(nextMessage);
? widget.dateDividerBuilder!(
nextMessage.createdAt.toLocal(),
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: DateDivider(
dateTime: nextMessage.createdAt.toLocal(),
),
);
return divider;
} }
final timeDiff = final timeDiff =
Jiffy(nextMessage.createdAt.toLocal()).diff( Jiffy(nextMessage.createdAt.toLocal()).diff(
@@ -747,6 +742,20 @@ class _MessageListViewState extends State<MessageListView> {
return child; return child;
} }
Widget _buildDateDivider(Message message) {
final divider = widget.dateDividerBuilder != null
? widget.dateDividerBuilder!(
message.createdAt.toLocal(),
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: DateDivider(
dateTime: message.createdAt.toLocal(),
),
);
return divider;
}
Widget _buildThreadSeparator() { Widget _buildThreadSeparator() {
if (widget.threadSeparatorBuilder != null) { if (widget.threadSeparatorBuilder != null) {
return widget.threadSeparatorBuilder!.call(context); return widget.threadSeparatorBuilder!.call(context);
@@ -803,7 +812,11 @@ class _MessageListViewState extends State<MessageListView> {
index = _getBottomElementIndex(values); index = _getBottomElementIndex(values);
} }
if (index == null) return const Offstage(); if ((index == null) ||
(!_isThreadConversation && index == itemCount - 2) ||
(_isThreadConversation && index == itemCount - 1)) {
return const Offstage();
}
if (index <= 2 || index >= itemCount - 3) { if (index <= 2 || index >= itemCount - 3) {
if (widget.reverse) { if (widget.reverse) {
@@ -1079,7 +1092,7 @@ class _MessageListViewState extends State<MessageListView> {
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false; final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
final hasUrlAttachment = final hasUrlAttachment =
message.attachments.any((it) => it.titleLink != null); message.attachments.any((it) => it.ogScrapeUrl != null);
final borderSide = final borderSide =
isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment) isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
@@ -236,6 +236,8 @@ class MessageReactionsModal extends StatelessWidget {
reaction.user!.name.split(' ')[0], reaction.user!.name.split(' ')[0],
style: chatThemeData.textTheme.footnoteBold, style: chatThemeData.textTheme.footnoteBold,
textAlign: TextAlign.center, textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 1,
), ),
], ],
), ),
@@ -574,11 +574,11 @@ class _MessageWidgetState extends State<MessageWidget>
bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true;
bool get hasNonUrlAttachments => widget.message.attachments bool get hasNonUrlAttachments => widget.message.attachments
.where((it) => it.titleLink == null || it.type == 'giphy') .where((it) => it.ogScrapeUrl == null || it.type == 'giphy')
.isNotEmpty; .isNotEmpty;
bool get hasUrlAttachments => widget.message.attachments bool get hasUrlAttachments => widget.message.attachments
.any((it) => it.titleLink != null && it.type != 'giphy'); .any((it) => it.ogScrapeUrl != null && it.type != 'giphy');
bool get showBottomRow => bool get showBottomRow =>
showThreadReplyIndicator || showThreadReplyIndicator ||
@@ -999,9 +999,9 @@ class _MessageWidgetState extends State<MessageWidget>
Widget _buildUrlAttachment() { Widget _buildUrlAttachment() {
final urlAttachment = widget.message.attachments final urlAttachment = widget.message.attachments
.firstWhere((element) => element.titleLink != null); .firstWhere((element) => element.ogScrapeUrl != null);
final host = Uri.parse(urlAttachment.titleLink!).host; final host = Uri.parse(urlAttachment.ogScrapeUrl!).withScheme.host;
final splitList = host.split('.'); final splitList = host.split('.');
final hostName = splitList.length == 3 ? splitList[1] : splitList[0]; final hostName = splitList.length == 3 ? splitList[1] : splitList[0];
final hostDisplayName = urlAttachment.authorName?.capitalize() ?? final hostDisplayName = urlAttachment.authorName?.capitalize() ??
@@ -1173,7 +1173,7 @@ class _MessageWidgetState extends State<MessageWidget>
widget.message.attachments widget.message.attachments
.where((element) => .where((element) =>
(element.titleLink == null && element.type != null) || (element.ogScrapeUrl == null && element.type != null) ||
element.type == 'giphy') element.type == 'giphy')
.forEach((e) { .forEach((e) {
if (attachmentGroups[e.type] == null) { if (attachmentGroups[e.type] == null) {
@@ -97,7 +97,7 @@ class QuotedMessageWidget extends StatelessWidget {
bool get _hasAttachments => message.attachments.isNotEmpty; bool get _hasAttachments => message.attachments.isNotEmpty;
bool get _containsLinkAttachment => bool get _containsLinkAttachment =>
message.attachments.any((element) => element.titleLink != null); message.attachments.any((element) => element.ogScrapeUrl != null);
bool get _containsText => message.text?.isNotEmpty == true; bool get _containsText => message.text?.isNotEmpty == true;
@@ -201,7 +201,7 @@ class QuotedMessageWidget extends StatelessWidget {
Attachment attachment; Attachment attachment;
if (_containsLinkAttachment) { if (_containsLinkAttachment) {
attachment = message.attachments.firstWhere( attachment = message.attachments.firstWhere(
(element) => element.titleLink != null, (element) => element.ogScrapeUrl != null,
); );
child = _buildUrlAttachment(attachment); child = _buildUrlAttachment(attachment);
} else { } else {
@@ -65,11 +65,13 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
this.onBackPressed, this.onBackPressed,
this.title, this.title,
this.subtitle, this.subtitle,
this.centerTitle,
this.leading, this.leading,
this.actions, this.actions,
this.onTitleTap, this.onTitleTap,
this.showTypingIndicator = true, this.showTypingIndicator = true,
this.backgroundColor, this.backgroundColor,
this.elevation = 1,
}) : preferredSize = const Size.fromHeight(kToolbarHeight), }) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key); super(key: key);
@@ -92,6 +94,9 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// Subtitle widget /// Subtitle widget
final Widget? subtitle; final Widget? subtitle;
/// Whether the title should be centered
final bool? centerTitle;
/// Leading widget /// Leading widget
final Widget? leading; final Widget? leading;
@@ -105,8 +110,17 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// The background color of this [ThreadHeader]. /// The background color of this [ThreadHeader].
final Color? backgroundColor; final Color? backgroundColor;
/// The elevation for this [ThreadHeader].
final double elevation;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final effectiveCenterTitle = getEffectiveCenterTitle(
Theme.of(context),
actions: actions,
centerTitle: centerTitle,
);
final channelHeaderTheme = ChannelHeaderTheme.of(context); final channelHeaderTheme = ChannelHeaderTheme.of(context);
final defaultSubtitle = subtitle ?? final defaultSubtitle = subtitle ??
@@ -134,7 +148,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
systemOverlayStyle: theme.brightness == Brightness.dark systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light ? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark, : SystemUiOverlayStyle.dark,
elevation: 1, elevation: elevation,
leading: leading ?? leading: leading ??
(showBackButton (showBackButton
? StreamBackButton( ? StreamBackButton(
@@ -144,7 +158,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
) )
: const SizedBox()), : const SizedBox()),
backgroundColor: backgroundColor ?? channelHeaderTheme.color, backgroundColor: backgroundColor ?? channelHeaderTheme.color,
centerTitle: true, centerTitle: centerTitle,
actions: actions, actions: actions,
title: InkWell( title: InkWell(
onTap: onTitleTap, onTap: onTitleTap,
@@ -153,6 +167,9 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
width: 250, width: 250,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: effectiveCenterTitle
? CrossAxisAlignment.center
: CrossAxisAlignment.stretch,
children: [ children: [
title ?? title ??
Text( Text(
@@ -47,6 +47,12 @@ class TypingIndicator extends StatelessWidget {
.where((element) => element.value.parentId == parentId) .where((element) => element.value.parentId == parentId)
.map((e) => e.key)), .map((e) => e.key)),
builder: (context, data) => AnimatedSwitcher( builder: (context, data) => AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) => Stack(
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
),
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: data.isNotEmpty child: data.isNotEmpty
? Padding( ? Padding(
@@ -8,15 +8,37 @@ import 'package:url_launcher/url_launcher.dart';
/// Launch URL /// Launch URL
Future<void> launchURL(BuildContext context, String url) async { Future<void> launchURL(BuildContext context, String url) async {
if (await canLaunch(url)) { try {
await launch(url); await launch(Uri.parse(url).withScheme.toString());
} else { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.translations.launchUrlError)), SnackBar(content: Text(context.translations.launchUrlError)),
); );
} }
} }
/// Get centerTitle considering a default and platform specific behaviour
bool getEffectiveCenterTitle(
ThemeData theme, {
bool? centerTitle,
List<Widget>? actions,
}) {
if (centerTitle != null) return centerTitle;
if (theme.appBarTheme.centerTitle != null) {
return theme.appBarTheme.centerTitle!;
}
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return false;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return actions == null || actions.length < 2;
}
}
/// Shows confirmation dialog /// Shows confirmation dialog
Future<bool?> showConfirmationDialog( Future<bool?> showConfirmationDialog(
BuildContext context, { BuildContext context, {
@@ -74,6 +74,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) {
return const StreamChatLocalizationsKo(); return const StreamChatLocalizationsKo();
case 'pt': case 'pt':
return const StreamChatLocalizationsPt(); return const StreamChatLocalizationsPt();
default:
return null;
} }
} }
@@ -1,5 +1,4 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart' show LogRecord;
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';