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:
```dart
Scaffold(
body: Stack(
children: <Widget>[
// Add your video implementation here
ShaderMask(
shaderCallback: (rect) {
return LinearGradient(
Stack(
children: <Widget>[
// Add your video implementation here
ShaderMask(
shaderCallback: (rect) {
return const LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [Colors.black, Colors.transparent],
stops: [0.4, 0.65]
).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height));
},
blendMode: BlendMode.dstIn,
child: Column(
children: [
Expanded(
child: MessageListView(),
),
MessageInput(),
],
),
),
],
),
)
```
colors: [Colors.black, Colors.transparent],
stops: [0.4, 0.8]).createShader(
Rect.fromLTRB(0, 0, rect.width, rect.height),
);
},
blendMode: BlendMode.dstIn,
child: Column(
children: const [
Expanded(
child: MessageListViewTheme(
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
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
🐞 Fixed
+24 -16
View File
@@ -64,7 +64,7 @@ class StreamChatClient {
StreamChatClient(
String apiKey, {
this.logLevel = Level.WARNING,
LogHandlerFunction? logHandlerFunction,
this.logHandlerFunction = StreamChatClient.defaultLogHandler,
RetryPolicy? retryPolicy,
@Deprecated('''
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,
AttachmentFileUploader? attachmentFileUploader,
}) {
this.logHandlerFunction = logHandlerFunction ?? _defaultLogHandler;
logger.info('Initiating new StreamChatClient');
final options = StreamHttpClientOptions(
@@ -134,7 +133,7 @@ class StreamChatClient {
'${CurrentPlatform.name}-'
'${PACKAGE_VERSION.split('+')[0]}';
/// Additionals headers for all requests
/// Additional headers for all requests
static Map<String, Object?> additionalHeaders = {};
ChatPersistenceClient? _originalChatPersistenceClient;
@@ -189,7 +188,7 @@ class StreamChatClient {
/// final client = StreamChatClient("stream-chat-api-key",
/// logHandlerFunction: myLogHandlerFunction);
///```
late LogHandlerFunction logHandlerFunction;
final LogHandlerFunction logHandlerFunction;
StreamSubscription<ConnectionStatus>? _connectionStatusSubscription;
@@ -214,17 +213,18 @@ class StreamChatClient {
Stream<ConnectionStatus> get wsConnectionStatusStream =>
_wsConnectionStatusController.stream.distinct();
LogHandlerFunction get _defaultLogHandler => (LogRecord record) {
print(
'${record.time} '
'${_levelEmojiMapper[record.level] ?? record.level.name} '
'${record.loggerName} ${record.message} ',
);
if (record.error != null) print(record.error);
if (record.stackTrace != null) print(record.stackTrace);
};
/// Default log handler function for the [StreamChatClient] logger.
static void defaultLogHandler(LogRecord record) {
print(
'${record.time} '
'${_levelEmojiMapper[record.level] ?? record.level.name} '
'${record.loggerName} ${record.message} ',
);
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)
..level = logLevel
..onRecord.listen(logHandlerFunction);
@@ -818,8 +818,16 @@ class StreamChatClient {
);
/// Add a device for Push Notifications.
Future<EmptyResponse> addDevice(String id, PushProvider pushProvider) =>
_chatApi.device.addDevice(id, pushProvider);
Future<EmptyResponse> addDevice(
String id,
PushProvider pushProvider, {
String? pushProviderName,
}) =>
_chatApi.device.addDevice(
id,
pushProvider,
pushProviderName: pushProviderName,
);
/// Gets a list of user devices.
Future<ListDevicesResponse> getDevices() => _chatApi.device.getDevices();
@@ -29,13 +29,16 @@ class DeviceApi {
/// Add a device for Push Notifications.
Future<EmptyResponse> addDevice(
String deviceId,
PushProvider pushProvider,
) async {
PushProvider pushProvider, {
String? pushProviderName,
}) async {
final response = await _client.post(
'/devices',
data: {
'id': deviceId,
'push_provider': pushProvider.name,
if (pushProviderName != null && pushProviderName.isNotEmpty)
'push_provider_name': pushProviderName,
},
);
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/options.dart';
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 './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/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);
});
test('`.addDevice`', () async {
test('`.addDevice should work`', () async {
const id = 'test-device-id';
const provider = PushProvider.firebase;
@@ -1185,6 +1185,34 @@ void main() {
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 {
final devices = List.generate(
3,
@@ -20,7 +20,7 @@ void main() {
deviceApi = DeviceApi(client);
});
test('addDevice', () async {
test('addDevice should work', () async {
const deviceId = 'test-device-id';
const pushProvider = PushProvider.firebase;
@@ -44,6 +44,36 @@ void main() {
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 {
const path = '/devices';
+9 -1
View File
@@ -3,6 +3,14 @@
✅ Added
- [[#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
@@ -12,7 +20,7 @@
🐞 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`
- Removed dependency on `visibility_detector`
@@ -18,14 +18,11 @@ class AttachmentTitle extends StatelessWidget {
@override
Widget build(BuildContext context) {
final normalizedTitleLink = attachment.titleLink?.replaceFirst(
RegExp(r'https?://(www\.)?'),
'',
);
final ogScrapeUrl = attachment.ogScrapeUrl;
return GestureDetector(
onTap: () {
final titleLink = attachment.titleLink;
if (titleLink != null) launchURL(context, titleLink);
final ogScrapeUrl = attachment.ogScrapeUrl;
if (ogScrapeUrl != null) launchURL(context, ogScrapeUrl);
},
child: Padding(
padding: const EdgeInsets.all(8),
@@ -42,8 +39,8 @@ class AttachmentTitle extends StatelessWidget {
fontWeight: FontWeight.bold,
),
),
if (normalizedTitleLink != null)
Text(normalizedTitleLink, style: messageTheme.messageTextStyle),
if (ogScrapeUrl != null)
Text(ogScrapeUrl, style: messageTheme.messageTextStyle),
],
),
),
@@ -37,11 +37,11 @@ class UrlAttachment extends StatelessWidget {
final chatThemeData = StreamChatTheme.of(context);
return GestureDetector(
onTap: () {
final titleLink = urlAttachment.titleLink;
if (titleLink != null) {
final ogScrapeUrl = urlAttachment.ogScrapeUrl;
if (ogScrapeUrl != null) {
onLinkTap != null
? onLinkTap!(titleLink)
: launchURL(context, titleLink);
? onLinkTap!(ogScrapeUrl)
: launchURL(context, ogScrapeUrl);
}
},
child: Column(
@@ -61,9 +61,11 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
this.showConnectionStateTile = false,
this.title,
this.subtitle,
this.centerTitle,
this.leading,
this.actions,
this.backgroundColor,
this.elevation = 1,
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key);
@@ -92,6 +94,9 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// Subtitle widget
final Widget? subtitle;
/// Whether the title should be centered
final bool? centerTitle;
/// Leading widget
final Widget? leading;
@@ -102,8 +107,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// The background color for this [ChannelHeader].
final Color? backgroundColor;
/// The elevation for this [ChannelHeader].
final double elevation;
@override
Widget build(BuildContext context) {
final effectiveCenterTitle = getEffectiveCenterTitle(
Theme.of(context),
actions: actions,
centerTitle: centerTitle,
);
final channel = StreamChannel.of(context).channel;
final channelHeaderTheme = ChannelHeaderTheme.of(context);
@@ -144,7 +157,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
elevation: 1,
elevation: elevation,
leading: leadingWidget,
backgroundColor: backgroundColor ?? channelHeaderTheme.color,
actions: actions ??
@@ -162,14 +175,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
),
),
],
centerTitle: true,
centerTitle: centerTitle,
title: InkWell(
onTap: onTitleTap,
child: SizedBox(
height: preferredSize.height,
width: preferredSize.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: effectiveCenterTitle
? CrossAxisAlignment.center
: CrossAxisAlignment.stretch,
children: <Widget>[
title ??
ChannelName(
@@ -56,9 +56,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
this.showConnectionStateTile = false,
this.preNavigationCallback,
this.subtitle,
this.centerTitle,
this.leading,
this.actions,
this.backgroundColor,
this.elevation = 1,
}) : super(key: key);
/// 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
final Widget? subtitle;
/// Whether the title should be centered
final bool? centerTitle;
/// Leading widget
/// By default it shows the logged in user avatar
final Widget? leading;
@@ -94,6 +99,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
/// The background color for this [ChannelListHeader].
final Color? backgroundColor;
/// The elevation for this [ChannelListHeader].
final double elevation;
@override
Widget build(BuildContext context) {
final _client = client ?? StreamChat.of(context).client;
@@ -128,10 +136,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
elevation: 1,
elevation: elevation,
backgroundColor:
backgroundColor ?? channelListHeaderThemeData.color,
centerTitle: true,
centerTitle: centerTitle,
leading: leading ??
Center(
child: user != null
@@ -225,3 +225,12 @@ extension UserListX on List<User> {
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() {
if (!_hasQuotedMessage) return const Offstage();
final containsUrl = widget.quotedMessage!.attachments
.any((element) => element.titleLink != null);
.any((element) => element.ogScrapeUrl != null);
return QuotedMessageWidget(
reverse: true,
showBorder: !containsUrl,
@@ -528,6 +528,7 @@ class _MessageListViewState extends State<MessageListView> {
return ((index + 2) * 2) - 1;
}
}
return null;
},
// 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
? widget.headerBuilder == null
: widget.footerBuilder == null) {
if (messages.isNotEmpty) {
return _buildDateDivider(messages.last);
}
if (_isThreadConversation) return const Offstage();
return const SizedBox(height: 52);
}
@@ -583,21 +587,12 @@ class _MessageListViewState extends State<MessageListView> {
message = messages[i - 2];
nextMessage = messages[i - 1];
}
if (!Jiffy(message.createdAt.toLocal()).isSame(
nextMessage.createdAt.toLocal(),
Units.DAY,
)) {
final divider = widget.dateDividerBuilder != null
? widget.dateDividerBuilder!(
nextMessage.createdAt.toLocal(),
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: DateDivider(
dateTime: nextMessage.createdAt.toLocal(),
),
);
return divider;
return _buildDateDivider(nextMessage);
}
final timeDiff =
Jiffy(nextMessage.createdAt.toLocal()).diff(
@@ -747,6 +742,20 @@ class _MessageListViewState extends State<MessageListView> {
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() {
if (widget.threadSeparatorBuilder != null) {
return widget.threadSeparatorBuilder!.call(context);
@@ -803,7 +812,11 @@ class _MessageListViewState extends State<MessageListView> {
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 (widget.reverse) {
@@ -1079,7 +1092,7 @@ class _MessageListViewState extends State<MessageListView> {
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
final hasUrlAttachment =
message.attachments.any((it) => it.titleLink != null);
message.attachments.any((it) => it.ogScrapeUrl != null);
final borderSide =
isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
@@ -236,6 +236,8 @@ class MessageReactionsModal extends StatelessWidget {
reaction.user!.name.split(' ')[0],
style: chatThemeData.textTheme.footnoteBold,
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 hasNonUrlAttachments => widget.message.attachments
.where((it) => it.titleLink == null || it.type == 'giphy')
.where((it) => it.ogScrapeUrl == null || it.type == 'giphy')
.isNotEmpty;
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 =>
showThreadReplyIndicator ||
@@ -999,9 +999,9 @@ class _MessageWidgetState extends State<MessageWidget>
Widget _buildUrlAttachment() {
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 hostName = splitList.length == 3 ? splitList[1] : splitList[0];
final hostDisplayName = urlAttachment.authorName?.capitalize() ??
@@ -1173,7 +1173,7 @@ class _MessageWidgetState extends State<MessageWidget>
widget.message.attachments
.where((element) =>
(element.titleLink == null && element.type != null) ||
(element.ogScrapeUrl == null && element.type != null) ||
element.type == 'giphy')
.forEach((e) {
if (attachmentGroups[e.type] == null) {
@@ -97,7 +97,7 @@ class QuotedMessageWidget extends StatelessWidget {
bool get _hasAttachments => message.attachments.isNotEmpty;
bool get _containsLinkAttachment =>
message.attachments.any((element) => element.titleLink != null);
message.attachments.any((element) => element.ogScrapeUrl != null);
bool get _containsText => message.text?.isNotEmpty == true;
@@ -201,7 +201,7 @@ class QuotedMessageWidget extends StatelessWidget {
Attachment attachment;
if (_containsLinkAttachment) {
attachment = message.attachments.firstWhere(
(element) => element.titleLink != null,
(element) => element.ogScrapeUrl != null,
);
child = _buildUrlAttachment(attachment);
} else {
@@ -65,11 +65,13 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
this.onBackPressed,
this.title,
this.subtitle,
this.centerTitle,
this.leading,
this.actions,
this.onTitleTap,
this.showTypingIndicator = true,
this.backgroundColor,
this.elevation = 1,
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
super(key: key);
@@ -92,6 +94,9 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// Subtitle widget
final Widget? subtitle;
/// Whether the title should be centered
final bool? centerTitle;
/// Leading widget
final Widget? leading;
@@ -105,8 +110,17 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// The background color of this [ThreadHeader].
final Color? backgroundColor;
/// The elevation for this [ThreadHeader].
final double elevation;
@override
Widget build(BuildContext context) {
final effectiveCenterTitle = getEffectiveCenterTitle(
Theme.of(context),
actions: actions,
centerTitle: centerTitle,
);
final channelHeaderTheme = ChannelHeaderTheme.of(context);
final defaultSubtitle = subtitle ??
@@ -134,7 +148,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
elevation: 1,
elevation: elevation,
leading: leading ??
(showBackButton
? StreamBackButton(
@@ -144,7 +158,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
)
: const SizedBox()),
backgroundColor: backgroundColor ?? channelHeaderTheme.color,
centerTitle: true,
centerTitle: centerTitle,
actions: actions,
title: InkWell(
onTap: onTitleTap,
@@ -153,6 +167,9 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
width: 250,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: effectiveCenterTitle
? CrossAxisAlignment.center
: CrossAxisAlignment.stretch,
children: [
title ??
Text(
@@ -47,6 +47,12 @@ class TypingIndicator extends StatelessWidget {
.where((element) => element.value.parentId == parentId)
.map((e) => e.key)),
builder: (context, data) => AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) => Stack(
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
),
duration: const Duration(milliseconds: 300),
child: data.isNotEmpty
? Padding(
@@ -8,15 +8,37 @@ import 'package:url_launcher/url_launcher.dart';
/// Launch URL
Future<void> launchURL(BuildContext context, String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
try {
await launch(Uri.parse(url).withScheme.toString());
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
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
Future<bool?> showConfirmationDialog(
BuildContext context, {
@@ -74,6 +74,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) {
return const StreamChatLocalizationsKo();
case 'pt':
return const StreamChatLocalizationsPt();
default:
return null;
}
}
@@ -1,5 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart' show LogRecord;
import 'package:mutex/mutex.dart';
import 'package:stream_chat/stream_chat.dart';