Merge branch 'v4' into docs/v4

This commit is contained in:
Salvatore Giordano
2022-04-29 16:26:09 +02:00
129 changed files with 3773 additions and 819 deletions
@@ -64,30 +64,35 @@ 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: StreamMessageListView(),
),
StreamMessageInput(),
],
),
),
],
),
)
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: StreamMessageListViewTheme(
data: StreamMessageListViewThemeData(
backgroundColor: Colors.transparent,
),
child: StreamMessageListView(),
),
),
StreamMessageInput(),
],
),
),
],
),
```
@@ -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
## 4.0.0-beta.2
🐞 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);
@@ -820,8 +820,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);
@@ -1,5 +1,6 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
part of 'attachment_file.dart';
@@ -324,13 +325,15 @@ class _$InProgress implements InProgress {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is InProgress &&
(identical(other.uploaded, uploaded) ||
other.uploaded == uploaded) &&
(identical(other.total, total) || other.total == total));
const DeepCollectionEquality().equals(other.uploaded, uploaded) &&
const DeepCollectionEquality().equals(other.total, total));
}
@override
int get hashCode => Object.hash(runtimeType, uploaded, total);
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(uploaded),
const DeepCollectionEquality().hash(total));
@JsonKey(ignore: true)
@override
@@ -612,11 +615,12 @@ class _$Failed implements Failed {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is Failed &&
(identical(other.error, error) || other.error == error));
const DeepCollectionEquality().equals(other.error, error));
}
@override
int get hashCode => Object.hash(runtimeType, error);
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(error));
@JsonKey(ignore: true)
@override
@@ -13,7 +13,7 @@ class ChannelModel {
String? id,
String? type,
String? cid,
this.ownCapabilities = const [],
this.ownCapabilities,
ChannelConfig? config,
this.createdBy,
this.frozen = false,
@@ -54,7 +54,7 @@ class ChannelModel {
/// List of user permissions on this channel
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final List<String> ownCapabilities;
final List<String>? ownCapabilities;
/// The channel configuration data
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@@ -11,9 +11,8 @@ ChannelModel _$ChannelModelFromJson(Map<String, dynamic> json) => ChannelModel(
type: json['type'] as String?,
cid: json['cid'] as String?,
ownCapabilities: (json['own_capabilities'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
?.map((e) => e as String)
.toList(),
config: json['config'] == null
? null
: ChannelConfig.fromJson(json['config'] as Map<String, dynamic>),
+29 -1
View File
@@ -5,7 +5,7 @@ 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 'package:uuid/uuid.dart';
@@ -41,3 +41,31 @@ export './src/permission_type.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';
+13
View File
@@ -1,3 +1,16 @@
## Upcoming
✅ 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.
## 4.0.0-beta.2
✅ Added
@@ -145,7 +145,7 @@ class ThreadPage extends StatelessWidget {
),
),
StreamMessageInput(
messageInputController: MessageInputController(
messageInputController: StreamMessageInputController(
message: Message(parentId: parent!.id),
),
),
@@ -184,7 +184,7 @@ class ThreadPage extends StatelessWidget {
),
),
StreamMessageInput(
messageInputController: MessageInputController(
messageInputController: StreamMessageInputController(
message: Message(parentId: parent!.id),
),
),
@@ -24,14 +24,11 @@ class StreamAttachmentTitle 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),
@@ -48,8 +45,8 @@ class StreamAttachmentTitle extends StatelessWidget {
fontWeight: FontWeight.bold,
),
),
if (normalizedTitleLink != null)
Text(normalizedTitleLink, style: messageTheme.messageTextStyle),
if (ogScrapeUrl != null)
Text(ogScrapeUrl, style: messageTheme.messageTextStyle),
],
),
),
@@ -43,11 +43,11 @@ class StreamUrlAttachment 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(
@@ -68,9 +68,11 @@ class StreamChannelHeader extends StatelessWidget
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);
@@ -99,6 +101,9 @@ class StreamChannelHeader extends StatelessWidget
/// Subtitle widget
final Widget? subtitle;
/// Whether the title should be centered
final bool? centerTitle;
/// Leading widget
final Widget? leading;
@@ -109,8 +114,16 @@ class StreamChannelHeader extends StatelessWidget
/// The background color for this [StreamChannelHeader].
final Color? backgroundColor;
/// The elevation for this [StreamChannelHeader].
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 = StreamChannelHeaderTheme.of(context);
@@ -151,7 +164,7 @@ class StreamChannelHeader extends StatelessWidget
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
elevation: 1,
elevation: elevation,
leading: leadingWidget,
backgroundColor: backgroundColor ?? channelHeaderTheme.color,
actions: actions ??
@@ -170,14 +183,16 @@ class StreamChannelHeader extends StatelessWidget
),
),
],
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 ??
StreamChannelName(
@@ -62,9 +62,11 @@ class StreamChannelListHeader extends StatelessWidget
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.
@@ -89,6 +91,9 @@ class StreamChannelListHeader extends StatelessWidget
/// 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;
@@ -100,6 +105,9 @@ class StreamChannelListHeader extends StatelessWidget
/// The background color for this [StreamChannelListHeader].
final Color? backgroundColor;
/// The elevation for this [StreamChannelListHeader].
final double elevation;
@override
Widget build(BuildContext context) {
final _client = client ?? StreamChat.of(context).client;
@@ -135,10 +143,10 @@ class StreamChannelListHeader extends StatelessWidget
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
@@ -1,3 +1,6 @@
// ignore: lines_longer_than_80_chars
// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:shimmer/shimmer.dart';
@@ -272,3 +272,12 @@ extension MessageX on Message {
return copyWith(text: messageTextToSend);
}
}
/// 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()}');
}
}
@@ -5,7 +5,6 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:stream_chat_flutter/src/stream_attachment_package.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
@@ -1,7 +1,6 @@
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/connection_status_builder.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/src/v4/message_input/stream_message_input.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'
show User;
@@ -321,6 +320,9 @@ abstract class Translations {
/// The label for "Reply to message"
String get replyToMessageLabel;
/// The label for "View library"
String get viewLibrary;
/// Label for "Attachment limit exceeded:
/// it's not possible to add more than $limit attachments"
String attachmentLimitExceedError(int limit);
@@ -696,6 +698,9 @@ class DefaultTranslations implements Translations {
@override
String get slowModeOnLabel => 'Slow mode ON';
@override
String get viewLibrary => 'View library';
@override
String attachmentLimitExceedError(int limit) => """
Attachment limit exceeded: it's not possible to add more than $limit attachments""";
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/src/media_list_view_controller.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro media_list_view}
@@ -20,6 +21,7 @@ class StreamMediaListView extends StatefulWidget {
Key? key,
this.selectedIds = const [],
this.onSelect,
this.controller,
}) : super(key: key);
/// Stores the media selected
@@ -28,18 +30,28 @@ class StreamMediaListView extends StatefulWidget {
/// Callback for on media selected
final void Function(AssetEntity media)? onSelect;
/// Controller that handles MediaListView
final MediaListViewController? controller;
@override
_StreamMediaListViewState createState() => _StreamMediaListViewState();
}
class _StreamMediaListViewState extends State<StreamMediaListView> {
final _media = <AssetEntity>[];
final ScrollController _scrollController = ScrollController();
int _currentPage = 0;
var _media = <AssetEntity>[];
var _currentPage = 0;
final _scrollController = ScrollController();
/// Controller necessary to verify limited access to photo gallery in iOS and
/// update the media list when listerners are emitted
late final controller = widget.controller ?? MediaListViewController();
@override
Widget build(BuildContext context) => LazyLoadScrollView(
onEndOfPage: () async => _getMedia(),
onEndOfPage: () async {
await _getMedia();
_updatePage();
},
child: GridView.builder(
itemCount: _media.length,
controller: _scrollController,
@@ -136,9 +148,29 @@ class _StreamMediaListViewState extends State<StreamMediaListView> {
@override
void initState() {
super.initState();
controller.addListener(_updateMediaList);
_getMedia();
}
@override
void dispose() {
super.dispose();
controller.removeListener(_updateMediaList);
if (widget.controller == null) {
controller.dispose();
}
}
void _updateMediaList() {
if (controller.shouldUpdateMedia) {
_getMedia();
}
}
void _updatePage() {
++_currentPage;
}
Future<void> _getMedia() async {
final assetList = (await PhotoManager.getAssetPathList(
filterOption: FilterOptionGroup(
@@ -157,13 +189,11 @@ class _StreamMediaListViewState extends State<StreamMediaListView> {
page: _currentPage,
size: 50,
);
if (media?.isNotEmpty == true) {
setState(() {
_media.addAll(media!);
_media = media!;
});
}
++_currentPage;
}
}
@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
/// Controller for MediaListView Widget
class MediaListViewController extends ChangeNotifier {
var _shouldUpdateMedia = false;
/// Getter that knows if the media should be updated.
bool get shouldUpdateMedia => _shouldUpdateMedia;
/// Method that update shouldUpdateMedia and notify all listeners
/// about this update.
void updateMedia({required bool newValue}) {
_shouldUpdateMedia = newValue;
notifyListeners();
}
}
@@ -639,7 +639,7 @@ class _StreamMessageActionsModalState extends State<StreamMessageActionsModal> {
widget.editMessageInputBuilder!(context, widget.message)
else
StreamMessageInput(
messageInputController: MessageInputController(
messageInputController: StreamMessageInputController(
message: widget.message,
),
preMessageSending: (m) {
@@ -1,3 +1,5 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:async';
import 'dart:math';
@@ -15,7 +17,7 @@ import 'package:stream_chat_flutter/src/emoji/emoji.dart';
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/media_list_view.dart';
import 'package:stream_chat_flutter/src/multi_overlay.dart';
import 'package:stream_chat_flutter/src/media_list_view_controller.dart';
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
import 'package:stream_chat_flutter/src/user_mentions_overlay.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
@@ -333,6 +335,7 @@ class MessageInputState extends State<MessageInput> {
final List<User> _mentionedUsers = [];
final _imagePicker = ImagePicker();
final _mediaListViewController = MediaListViewController();
late final _focusNode = widget.focusNode ?? FocusNode();
late final _isInternalFocusNode = widget.focusNode == null;
bool _inputEnabled = true;
@@ -1046,6 +1049,26 @@ class MessageInputState extends State<MessageInput> {
);
},
),
const Spacer(),
FutureBuilder(
future: PhotoManager.requestPermissionExtend(),
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.data == PermissionState.limited) {
return TextButton(
child: Text(context.translations.viewLibrary),
onPressed: () async {
await PhotoManager.presentLimited();
_mediaListViewController.updateMedia(
newValue: true,
);
},
);
}
return const SizedBox.shrink();
},
),
],
),
DecoratedBox(
@@ -1078,6 +1101,7 @@ class MessageInputState extends State<MessageInput> {
borderRadius: BorderRadius.circular(8),
),
child: _PickerWidget(
mediaListViewController: _mediaListViewController,
filePickerIndex: _filePickerIndex,
streamChatTheme: _streamChatTheme,
containsFile: _attachmentContainsFile,
@@ -1241,7 +1265,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 StreamQuotedMessageWidget(
reverse: true,
showBorder: !containsUrl,
@@ -1909,6 +1933,7 @@ class _PickerWidget extends StatefulWidget {
required this.onAddMoreFilesClick,
required this.onMediaSelected,
required this.streamChatTheme,
required this.mediaListViewController,
}) : super(key: key);
final int filePickerIndex;
@@ -1917,6 +1942,7 @@ class _PickerWidget extends StatefulWidget {
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
final void Function(AssetEntity) onMediaSelected;
final StreamChatThemeData streamChatTheme;
final MediaListViewController mediaListViewController;
@override
_PickerWidgetState createState() => _PickerWidgetState();
@@ -1964,7 +1990,9 @@ class _PickerWidgetState extends State<_PickerWidget> {
),
);
}
return StreamMediaListView(
controller: widget.mediaListViewController,
selectedIds: widget.selectedMedias,
onSelect: widget.onMediaSelected,
);
@@ -579,6 +579,9 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
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);
}
@@ -603,21 +606,12 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
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: StreamDateDivider(
dateTime: nextMessage.createdAt.toLocal(),
),
);
return divider;
return _buildDateDivider(nextMessage);
}
final timeDiff =
Jiffy(nextMessage.createdAt.toLocal()).diff(
@@ -769,6 +763,20 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
return child;
}
Widget _buildDateDivider(Message message) {
final divider = widget.dateDividerBuilder != null
? widget.dateDividerBuilder!(
message.createdAt.toLocal(),
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: StreamDateDivider(
dateTime: message.createdAt.toLocal(),
),
);
return divider;
}
Widget _buildThreadSeparator() {
if (widget.threadSeparatorBuilder != null) {
return widget.threadSeparatorBuilder!.call(context);
@@ -825,7 +833,11 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
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) {
@@ -1109,7 +1121,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
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)
@@ -246,6 +246,8 @@ class StreamMessageReactionsModal extends StatelessWidget {
reaction.user!.name.split(' ')[0],
style: chatThemeData.textTheme.footnoteBold,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
@@ -1,3 +1,6 @@
// ignore: lines_longer_than_80_chars
// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -581,11 +581,11 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
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 ||
@@ -1006,9 +1006,9 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
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() ??
@@ -1176,7 +1176,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
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) {
@@ -57,7 +57,7 @@ class StreamQuotedMessageWidget 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;
@@ -161,7 +161,7 @@ class StreamQuotedMessageWidget extends StatelessWidget {
Attachment attachment;
if (_containsLinkAttachment) {
attachment = message.attachments.firstWhere(
(element) => element.titleLink != null,
(element) => element.ogScrapeUrl != null,
);
child = _buildUrlAttachment(attachment);
} else {
@@ -414,7 +414,7 @@ class StreamChatThemeData {
/// Theme configuration for the [StreamUserListView] widget.
final StreamUserListViewThemeData userListViewTheme;
/// Theme configuration for the [MessageSearchListView] widget.
/// Theme configuration for the [StreamMessageSearchListView] widget.
final StreamMessageSearchListViewThemeData messageSearchListViewTheme;
/// Creates a copy of [StreamChatThemeData] with specified attributes
@@ -72,11 +72,13 @@ class StreamThreadHeader extends StatelessWidget
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);
@@ -99,6 +101,9 @@ class StreamThreadHeader extends StatelessWidget
/// Subtitle widget
final Widget? subtitle;
/// Whether the title should be centered
final bool? centerTitle;
/// Leading widget
final Widget? leading;
@@ -112,8 +117,17 @@ class StreamThreadHeader extends StatelessWidget
/// The background color of this [StreamThreadHeader].
final Color? backgroundColor;
/// The elevation for this [StreamThreadHeader].
final double elevation;
@override
Widget build(BuildContext context) {
final effectiveCenterTitle = getEffectiveCenterTitle(
Theme.of(context),
actions: actions,
centerTitle: centerTitle,
);
final channelHeaderTheme = StreamChannelHeaderTheme.of(context);
final defaultSubtitle = subtitle ??
@@ -126,7 +140,8 @@ class StreamThreadHeader extends StatelessWidget
style: channelHeaderTheme.subtitleStyle,
),
Flexible(
child: ChannelName(
child: StreamChannelName(
channel: StreamChannel.of(context).channel,
textStyle: channelHeaderTheme.subtitleStyle,
),
),
@@ -141,7 +156,7 @@ class StreamThreadHeader extends StatelessWidget
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
elevation: 1,
elevation: elevation,
leading: leading ??
(showBackButton
? StreamBackButton(
@@ -151,7 +166,7 @@ class StreamThreadHeader extends StatelessWidget
)
: const SizedBox()),
backgroundColor: backgroundColor ?? channelHeaderTheme.color,
centerTitle: true,
centerTitle: centerTitle,
actions: actions,
title: InkWell(
onTap: onTitleTap,
@@ -160,6 +175,9 @@ class StreamThreadHeader extends StatelessWidget
width: 250,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: effectiveCenterTitle
? CrossAxisAlignment.center
: CrossAxisAlignment.stretch,
children: [
title ??
Text(
@@ -49,6 +49,12 @@ class StreamTypingIndicator extends StatelessWidget {
.where((element) => element.value.parentId == parentId)
.map((e) => e.key)),
builder: (context, users) => AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) => Stack(
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
),
duration: const Duration(milliseconds: 300),
child: users.isNotEmpty
? Padding(
@@ -1,3 +1,6 @@
// ignore: lines_longer_than_80_chars
// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -2,22 +2,43 @@ import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_attachment_package.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
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, {
@@ -433,8 +454,8 @@ int levenshtein(String s, String t, {bool caseSensitive = true}) {
/// An easy way to handle attachment related operations on a message
extension AttachmentPackagesX on Message {
/// This extension will return a List of type [StreamAttachmentPackage] from the
/// existing attachments of the message
/// This extension will return a List of type [StreamAttachmentPackage]
/// from the existing attachments of the message
List<StreamAttachmentPackage> getAttachmentPackageList() {
final _attachmentPackages = List<StreamAttachmentPackage>.generate(
attachments.length,
@@ -1,96 +0,0 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// A shimmering list item which shows a loading effect.
///
/// This is used by [StreamChannelListView] to show a loading effect while
/// the list is being loaded.
class StreamChannelListLoadingTile extends StatelessWidget {
/// Creates a new instance of [StreamChannelListLoadingTile] widget.
const StreamChannelListLoadingTile({
Key? key,
this.visualDensity = VisualDensity.standard,
this.contentPadding = const EdgeInsets.symmetric(horizontal: 8),
}) : super(key: key);
/// Defines how compact the list tile's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
///
/// See also:
///
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
/// widgets within a [Theme].
final VisualDensity visualDensity;
/// The tile's internal padding.
///
/// Insets a [ListTile]'s contents: its [leading], [title], [subtitle],
/// and [trailing] widgets.
///
/// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used.
final EdgeInsetsGeometry contentPadding;
@override
Widget build(BuildContext context) {
final colorTheme = StreamChatTheme.of(context).colorTheme;
final leading = Container(
height: 49,
width: 49,
decoration: BoxDecoration(
color: colorTheme.barsBg,
shape: BoxShape.circle,
),
);
final title = Container(
height: 16,
width: 66,
decoration: BoxDecoration(
color: colorTheme.barsBg,
borderRadius: BorderRadius.circular(8),
),
);
final subtitle = Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: Container(
height: 16,
decoration: BoxDecoration(
color: colorTheme.barsBg,
borderRadius: BorderRadius.circular(8),
),
),
),
),
const SizedBox(width: 8),
Container(
height: 16,
width: 50,
decoration: BoxDecoration(
color: colorTheme.barsBg,
borderRadius: BorderRadius.circular(8),
),
),
],
);
return Shimmer.fromColors(
baseColor: colorTheme.disabled,
highlightColor: colorTheme.inputBg,
child: ListTile(
leading: leading,
title: title,
subtitle: subtitle,
visualDensity: visualDensity,
contentPadding: contentPadding,
),
);
}
}
@@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/media_list_view.dart';
import 'package:stream_chat_flutter/src/media_list_view_controller.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Callback for when a file has to be picked.
@@ -47,8 +48,8 @@ class StreamAttachmentPicker extends StatefulWidget {
/// The picker size in height.
final double pickerSize;
/// The [MessageInputController] linked to this picker.
final MessageInputController messageInputController;
/// The [StreamMessageInputController] linked to this picker.
final StreamMessageInputController messageInputController;
/// The limit of attachments that can be picked.
final int attachmentLimit;
@@ -77,7 +78,7 @@ class StreamAttachmentPicker extends StatefulWidget {
/// properties.
StreamAttachmentPicker copyWith({
Key? key,
MessageInputController? messageInputController,
StreamMessageInputController? messageInputController,
FilePickerCallback? onFilePicked,
bool? isOpen,
double? pickerSize,
@@ -113,6 +114,7 @@ class StreamAttachmentPicker extends StatefulWidget {
class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
int _filePickerIndex = 0;
final _mediaListViewController = MediaListViewController();
@override
Widget build(BuildContext context) {
@@ -281,6 +283,26 @@ class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
),
],
),
const Spacer(),
FutureBuilder(
future: PhotoManager.requestPermissionExtend(),
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.data == PermissionState.limited) {
return TextButton(
child: Text(context.translations.viewLibrary),
onPressed: () async {
await PhotoManager.presentLimited();
_mediaListViewController.updateMedia(
newValue: true,
);
},
);
}
return const SizedBox.shrink();
},
),
DecoratedBox(
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.barsBg,
@@ -315,6 +337,7 @@ class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
borderRadius: BorderRadius.circular(8),
),
child: _PickerWidget(
mediaListViewController: _mediaListViewController,
filePickerIndex: _filePickerIndex,
streamChatTheme: _streamChatTheme,
containsFile: _attachmentContainsFile,
@@ -410,6 +433,7 @@ class _PickerWidget extends StatefulWidget {
required this.streamChatTheme,
required this.allowedAttachmentTypes,
required this.customAttachmentTypes,
required this.mediaListViewController,
}) : super(key: key);
final int filePickerIndex;
@@ -420,6 +444,7 @@ class _PickerWidget extends StatefulWidget {
final StreamChatThemeData streamChatTheme;
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
final List<CustomAttachmentType> customAttachmentTypes;
final MediaListViewController mediaListViewController;
@override
_PickerWidgetState createState() => _PickerWidgetState();
@@ -473,6 +498,7 @@ class _PickerWidgetState extends State<_PickerWidget> {
return StreamMediaListView(
selectedIds: widget.selectedMedias,
onSelect: widget.onMediaSelected,
controller: widget.mediaListViewController,
);
}
@@ -13,7 +13,6 @@ import 'package:stream_chat_flutter/src/commands_overlay.dart';
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/multi_overlay.dart';
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
import 'package:stream_chat_flutter/src/user_mentions_overlay.dart';
import 'package:stream_chat_flutter/src/v4/message_input/simple_safe_area.dart';
@@ -76,16 +75,16 @@ typedef ActionButtonBuilder = Widget Function(
);
/// Widget builder for widgets that may require data from the
/// [MessageInputController].
/// [StreamMessageInputController].
typedef MessageRelatedBuilder = Widget Function(
BuildContext context,
MessageInputController messageInputController,
StreamMessageInputController messageInputController,
);
/// Widget builder for a custom attachment picker.
typedef AttachmentsPickerBuilder = Widget Function(
BuildContext context,
MessageInputController messageInputController,
StreamMessageInputController messageInputController,
StreamAttachmentPicker defaultPicker,
);
@@ -246,7 +245,7 @@ class StreamMessageInput extends StatefulWidget {
final bool hideSendAsDm;
/// The text controller of the TextField.
final MessageInputController? messageInputController;
final StreamMessageInputController? messageInputController;
/// List of action widgets.
final List<Widget> actions;
@@ -375,14 +374,14 @@ class StreamMessageInputState extends State<StreamMessageInput>
bool get _disableEmojiSuggestionsOverlay =>
widget.disableEmojiSuggestionsOverlay ?? false;
RestorableMessageInputController? _controller;
StreamRestorableMessageInputController? _controller;
MessageInputController get _effectiveController =>
StreamMessageInputController get _effectiveController =>
widget.messageInputController ?? _controller!.value;
void _createLocalController([Message? message]) {
assert(_controller == null, '');
_controller = RestorableMessageInputController(message: message);
_controller = StreamRestorableMessageInputController(message: message);
}
void _registerController() {
@@ -502,7 +501,7 @@ class StreamMessageInputState extends State<StreamMessageInput>
),
);
}
return MessageValueListenableBuilder(
return StreamMessageValueListenableBuilder(
valueListenable: _effectiveController,
builder: (context, value, _) {
Widget child = DecoratedBox(
@@ -181,8 +181,8 @@ class StreamMessageTextField extends StatefulWidget {
/// Controls the message being edited.
///
/// If null, this widget will create its own [MessageInputController].
final MessageInputController? controller;
/// If null, this widget will create its own [StreamMessageInputController].
final StreamMessageInputController? controller;
/// Defines the keyboard focus for this widget.
///
@@ -437,7 +437,7 @@ class StreamMessageTextField extends StatefulWidget {
/// This setting is only honored on iOS devices.
///
/// If unset, defaults to the brightness of
/// [ThemeData.primaryColorBrightness].
/// [ThemeData.brightness].
final Brightness? keyboardAppearance;
/// {@macro flutter.widgets.editableText.scrollPadding}
@@ -656,9 +656,9 @@ class StreamMessageTextField extends StatefulWidget {
class _StreamMessageTextFieldState extends State<StreamMessageTextField>
with RestorationMixin<StreamMessageTextField> {
RestorableMessageInputController? _controller;
StreamRestorableMessageInputController? _controller;
MessageInputController get _effectiveController =>
StreamMessageInputController get _effectiveController =>
widget.controller ?? _controller!.value;
@override
@@ -671,7 +671,7 @@ class _StreamMessageTextFieldState extends State<StreamMessageTextField>
void _createLocalController([Message? message]) {
assert(_controller == null, '');
_controller = RestorableMessageInputController(message: message);
_controller = StreamRestorableMessageInputController(message: message);
}
@override
@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that displays a user.
///
/// This widget is intended to be used as a Tile in
/// [StreamChannelGridView].
///
/// It shows the user's avatar and name.
///
/// See also:
/// * [StreamChannelGridView]
/// * [StreamUserAvatar]
class StreamChannelGridTile extends StatelessWidget {
/// Creates a new instance of [StreamChannelGridTile] widget.
const StreamChannelGridTile({
Key? key,
required this.channel,
this.child,
this.footer,
this.onTap,
this.onLongPress,
}) : super(key: key);
/// The channel to display.
final Channel channel;
/// The widget to display in the body of the tile.
final Widget? child;
/// The widget to display in the footer of the tile.
final Widget? footer;
/// Called when the user taps this grid tile.
final GestureTapCallback? onTap;
/// Called when the user long-presses on this grid tile.
final GestureLongPressCallback? onLongPress;
/// Creates a copy of this tile but with the given fields replaced with
/// the new values.
StreamChannelGridTile copyWith({
Key? key,
Channel? channel,
Widget? child,
Widget? footer,
GestureTapCallback? onTap,
GestureLongPressCallback? onLongPress,
}) =>
StreamChannelGridTile(
key: key ?? this.key,
channel: channel ?? this.channel,
footer: footer ?? this.footer,
onTap: onTap ?? this.onTap,
onLongPress: onLongPress ?? this.onLongPress,
child: child ?? this.child,
);
@override
Widget build(BuildContext context) {
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
final child = this.child ??
StreamChannelAvatar(
channel: channel,
borderRadius: BorderRadius.circular(32),
constraints: const BoxConstraints.tightFor(
height: 64,
width: 64,
),
);
final footer = this.footer ??
StreamChannelName(
channel: channel,
textStyle: channelPreviewTheme.titleStyle,
);
return InkWell(
onTap: onTap,
onLongPress: onLongPress,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
child,
footer,
],
),
);
}
}
@@ -0,0 +1,401 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_empty_widget.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Default grid delegate for [StreamChannelGridView].
const defaultChannelGridViewDelegate =
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4);
/// Signature for the item builder that creates the children of the
/// [StreamChannelGridView].
typedef StreamChannelGridViewIndexedWidgetBuilder
= StreamScrollViewIndexedWidgetBuilder<Channel, StreamChannelGridTile>;
/// A [GridView] that shows a grid of [User]s,
/// it uses [StreamChannelGridTile] as a default item.
///
/// Example:
///
/// ```dart
/// StreamChannelGridView(
/// controller: controller,
/// onChannelTap: (channel) {
/// // Handle channel tap event
/// },
/// onChannelLongPress: (channel) {
/// // Handle channel long press event
/// },
/// )
/// ```
///
/// See also:
/// * [StreamChannelGridTile]
/// * [StreamChannelListController]
class StreamChannelGridView extends StatelessWidget {
/// Creates a new instance of [StreamChannelGridView].
const StreamChannelGridView({
Key? key,
required this.controller,
this.gridDelegate = defaultChannelGridViewDelegate,
this.itemBuilder,
this.emptyBuilder,
this.loadMoreErrorBuilder,
this.loadMoreIndicatorBuilder,
this.loadingBuilder,
this.errorBuilder,
this.onChannelTap,
this.onChannelLongPress,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : super(key: key);
/// The [StreamUserListController] used to control the grid of users.
final StreamChannelListController controller;
/// A delegate that controls the layout of the children within
/// the [PagedValueGridView].
final SliverGridDelegate gridDelegate;
/// A builder that is called to build items in the [PagedValueGridView].
///
/// The `value` parameter is the [Channel] at this position in the grid.
final StreamChannelGridViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the empty state of the grid.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the load more error state of the grid.
final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder;
/// A builder that is called to build the load more indicator of the grid.
final WidgetBuilder? loadMoreIndicatorBuilder;
/// A builder that is called to build the loading state of the grid.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the grid.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the user taps this grid tile.
final void Function(Channel)? onChannelTap;
/// Called when the user long-presses on this grid tile.
final void Function(Channel)? onChannelLongPress;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by
/// an otherwise focused part of the application,
/// the ScrollAction will be evaluated using this scroll view, for example,
/// when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// The number of children that will contribute semantic information.
///
/// Some subtypes of [ScrollView] can infer this value automatically. For
/// example [ListView] will use the number of widgets in the child list,
/// while the [ListView.separated] constructor will use half that amount.
///
/// For [CustomScrollView] and other types which do not receive a builder
/// or list of widgets, the child count must be explicitly provided. If the
/// number is unknown or unbounded this should be left unset or set to null.
///
/// See also:
///
/// * [SemanticsConfiguration.scrollChildCount],
/// the corresponding semantics property.
final int? semanticChildCount;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
Widget build(BuildContext context) {
return PagedValueGridView<int, Channel>(
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
scrollController: scrollController,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
clipBehavior: clipBehavior,
gridDelegate: gridDelegate,
itemBuilder: (context, channels, index) {
final channel = channels[index];
final onTap = onChannelTap;
final onLongPress = onChannelLongPress;
final streamChannelGridTile = StreamChannelGridTile(
channel: channel,
onTap: onTap == null ? null : () => onTap(channel),
onLongPress: onLongPress == null ? null : () => onLongPress(channel),
);
return itemBuilder?.call(
context,
channels,
index,
streamChannelGridTile,
) ??
streamChannelGridTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.message(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.letsStartChattingLabel,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamScrollViewLoadMoreError.grid(
onTap: controller.retry,
error: Text(
context.translations.loadingChannelsError,
textAlign: TextAlign.center,
),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingChannelsError),
onRetryPressed: controller.refresh,
),
),
);
}
}
@@ -1,7 +1,6 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/v4/stream_message_preview_text.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that displays a channel preview.
@@ -3,9 +3,14 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_loading_tile.dart';
import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart';
import 'package:stream_chat_flutter/src/v4/stream_list_view_indexed_widget_builder.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_empty_widget.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Default separator builder for [StreamChannelListView].
@@ -19,7 +24,7 @@ Widget defaultChannelListViewSeparatorBuilder(
/// Signature for the item builder that creates the children of the
/// [StreamChannelListView].
typedef StreamChannelListViewIndexedWidgetBuilder
= StreamListViewIndexedWidgetBuilder<Channel, StreamChannelListTile>;
= StreamScrollViewIndexedWidgetBuilder<Channel, StreamChannelListTile>;
/// A [ListView] that shows a list of [Channel]s,
/// it uses [StreamChannelListTile] as a default item.
@@ -78,23 +83,15 @@ class StreamChannelListView extends StatelessWidget {
final StreamChannelListController controller;
/// A builder that is called to build items in the [ListView].
///
/// The `channel` parameter is the [Channel] at this position in the list
/// and the `defaultWidget` is the default widget used
/// i.e: [StreamChannelListTile].
final StreamChannelListViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the list separator.
final PagedValueScrollViewIndexedWidgetBuilder<Channel> separatorBuilder;
/// A builder that is called to build the empty state of the list.
///
/// If not provided, [StreamChannelListEmptyWidget] will be used.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the loading state of the list.
///
/// If not provided, [StreamChannelListLoadingTile] will be used.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the list.
@@ -328,96 +325,52 @@ class StreamChannelListView extends StatelessWidget {
) ??
streamChannelListTile;
},
emptyBuilder: (context) =>
emptyBuilder?.call(context) ??
const Center(
child: Padding(
padding: EdgeInsets.all(8),
child: StreamChannelListEmptyWidget(),
),
),
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.message(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.letsStartChattingLabel,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamChannelListLoadMoreError(onTap: controller.retry),
StreamScrollViewLoadMoreError.list(
onTap: controller.retry,
error: Text(context.translations.loadingChannelsError),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamChannelListLoadMoreIndicator(),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
ListView.separated(
padding: padding,
physics: physics,
reverse: reverse,
itemCount: 25,
separatorBuilder: (_, __) => const StreamChannelListSeparator(),
itemBuilder: (_, __) => const StreamChannelListLoadingTile(),
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamChannelListErrorWidget(
onPressed: controller.refresh,
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingChannelsError),
onRetryPressed: controller.refresh,
),
),
);
}
/// A [StreamChannelListTile] that can be used in a [ListView] to show a
/// loading tile while waiting for the [StreamChannelListController] to load
/// more channels.
class StreamChannelListLoadMoreIndicator extends StatelessWidget {
/// Creates a new instance of [StreamChannelListLoadMoreIndicator].
const StreamChannelListLoadMoreIndicator({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator.adaptive(),
);
}
/// A [StreamChannelListTile] that is used to display the error indicator when
/// loading more channels fails.
class StreamChannelListLoadMoreError extends StatelessWidget {
/// Creates a new instance of [StreamChannelListLoadMoreError].
const StreamChannelListLoadMoreError({
Key? key,
this.onTap,
}) : super(key: key);
/// The callback to invoke when the user taps on the error indicator.
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context);
return InkWell(
onTap: onTap,
child: Container(
color: theme.colorTheme.textLowEmphasis.withOpacity(0.9),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.translations.loadingChannelsError,
style: theme.textTheme.body.copyWith(
color: Colors.white,
),
),
StreamSvgIcon.retry(color: Colors.white),
],
),
),
),
);
}
}
/// A widget that is used to display a separator between
/// [StreamChannelListTile] items.
class StreamChannelListSeparator extends StatelessWidget {
@@ -471,29 +424,3 @@ class StreamChannelListErrorWidget extends StatelessWidget {
],
);
}
/// A widget that is used to display an empty state when
/// [StreamChannelListController] loads zero channels.
class StreamChannelListEmptyWidget extends StatelessWidget {
/// Creates a new instance of [StreamChannelListEmptyWidget] widget.
const StreamChannelListEmptyWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.message(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
const SizedBox(height: 28),
Text(
context.translations.letsStartChattingLabel,
style: chatThemeData.textTheme.headline,
),
],
);
}
}
@@ -0,0 +1,365 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default grid delegate for [StreamMessageSearchGridView].
const defaultMessageSearchGridViewDelegate =
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4);
/// Signature for the item builder that creates the children of the
/// [StreamMessageSearchGridView].
typedef StreamMessageSearchGridViewIndexedWidgetBuilder
= PagedValueScrollViewIndexedWidgetBuilder<GetMessageResponse>;
/// A [GridView] that shows a grid of [GetMessageResponse]s,
/// it uses [StreamMessageSearchGridTile] as a default item.
///
/// Example:
///
/// ```dart
/// StreamMessageSearchGridView(
/// controller: controller,
/// itemBuilder: (context, messageResponses, index) {
/// return GridTile(message: messageResponses[index]);
/// },
/// )
/// ```
///
/// See also:
/// * [StreamUserListTile]
/// * [StreamUserListController]
class StreamMessageSearchGridView extends StatelessWidget {
/// Creates a new instance of [StreamMessageSearchGridView].
const StreamMessageSearchGridView({
Key? key,
required this.controller,
required this.itemBuilder,
this.gridDelegate = defaultMessageSearchGridViewDelegate,
this.emptyBuilder,
this.loadMoreErrorBuilder,
this.loadMoreIndicatorBuilder,
this.loadingBuilder,
this.errorBuilder,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : super(key: key);
/// The [StreamUserListController] used to control the grid of users.
final StreamMessageSearchListController controller;
/// A delegate that controls the layout of the children within
/// the [PagedValueGridView].
final SliverGridDelegate gridDelegate;
/// A builder that is called to build items in the [PagedValueGridView].
///
/// The `value` parameter is the [GetMessageBuilder]
/// at this position in the grid.
final StreamMessageSearchGridViewIndexedWidgetBuilder itemBuilder;
/// A builder that is called to build the empty state of the grid.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the load more error state of the grid.
final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder;
/// A builder that is called to build the load more indicator of the grid.
final WidgetBuilder? loadMoreIndicatorBuilder;
/// A builder that is called to build the loading state of the grid.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the grid.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by
/// an otherwise focused part of the application,
/// the ScrollAction will be evaluated using this scroll view, for example,
/// when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// The number of children that will contribute semantic information.
///
/// Some subtypes of [ScrollView] can infer this value automatically. For
/// example [ListView] will use the number of widgets in the child list,
/// while the [ListView.separated] constructor will use half that amount.
///
/// For [CustomScrollView] and other types which do not receive a builder
/// or list of widgets, the child count must be explicitly provided. If the
/// number is unknown or unbounded this should be left unset or set to null.
///
/// See also:
///
/// * [SemanticsConfiguration.scrollChildCount],
/// the corresponding semantics property.
final int? semanticChildCount;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
Widget build(BuildContext context) {
return PagedValueGridView<String, GetMessageResponse>(
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
scrollController: scrollController,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
clipBehavior: clipBehavior,
gridDelegate: gridDelegate,
itemBuilder: itemBuilder,
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.message(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.emptyMessagesText,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamScrollViewLoadMoreError.grid(
onTap: controller.retry,
error: Text(context.translations.loadingMessagesError),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
onRetryPressed: controller.refresh,
),
),
);
}
}
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/v4/stream_message_preview_text.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that displays a message search item.
@@ -158,15 +157,19 @@ class StreamMessageSearchListTile extends StatelessWidget {
}
}
/// A widget that displays the title of a [StreamMessageSearchListTile].
class MessageSearchListTileTitle extends StatelessWidget {
/// Creates a new [MessageSearchListTileTitle] instance.
const MessageSearchListTileTitle({
Key? key,
required this.messageResponse,
this.textStyle,
}) : super(key: key);
/// The message response for the tile.
final GetMessageResponse messageResponse;
/// The style to use for the title.
final TextStyle? textStyle;
@override
@@ -200,6 +203,7 @@ class MessageSearchListTileTitle extends StatelessWidget {
}
}
/// A widget which shows formatted created date of the passed [message].
class MessageSearchTileMessageDate extends StatelessWidget {
/// Creates a new instance of [MessageSearchTileMessageDate].
const MessageSearchTileMessageDate({
@@ -1,6 +1,12 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default separator builder for [StreamMessageSearchListView].
@@ -14,7 +20,7 @@ Widget defaultMessageSearchListViewSeparatorBuilder(
/// Signature for the item builder that creates the children of the
/// [StreamMessageSearchListView].
typedef StreamMessageSearchListViewIndexedWidgetBuilder
= StreamListViewIndexedWidgetBuilder<GetMessageResponse,
= StreamScrollViewIndexedWidgetBuilder<GetMessageResponse,
StreamMessageSearchListTile>;
/// A [ListView] that shows a list of [GetMessageResponse]s,
@@ -75,10 +81,6 @@ class StreamMessageSearchListView extends StatelessWidget {
final StreamMessageSearchListController controller;
/// A builder that is called to build items in the [ListView].
///
/// The `messageResponse` parameter is the [GetMessageResponse] at this
/// position in the list and the `defaultWidget` is the default widget used
/// i.e: [StreamMessageSearchListTile].
final StreamMessageSearchListViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the list separator.
@@ -86,18 +88,12 @@ class StreamMessageSearchListView extends StatelessWidget {
separatorBuilder;
/// A builder that is called to build the empty state of the list.
///
/// If not provided, [StreamMessageSearchListEmptyWidget] will be used.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the loading state of the list.
///
/// If not provided, [StreamMessageSearchListLoadingTile] will be used.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the list.
///
/// If not provided, [StreamMessageSearchListErrorWidget] will be used.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the user taps this list tile.
@@ -312,7 +308,7 @@ class StreamMessageSearchListView extends StatelessWidget {
final onTap = onMessageTap;
final onLongPress = onMessageLongPress;
final streamUserListTile = StreamMessageSearchListTile(
final streamMessageSearchListTile = StreamMessageSearchListTile(
messageResponse: messageResponse,
onTap: onTap == null ? null : () => onTap(messageResponse),
onLongPress:
@@ -323,101 +319,56 @@ class StreamMessageSearchListView extends StatelessWidget {
context,
messageResponses,
index,
streamUserListTile,
streamMessageSearchListTile,
) ??
streamUserListTile;
streamMessageSearchListTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.message(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.emptyMessagesText,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
emptyBuilder: (context) =>
emptyBuilder?.call(context) ??
const Center(
child: Padding(
padding: EdgeInsets.all(8),
child: StreamMessageSearchListEmptyWidget(),
),
),
loadMoreErrorBuilder: (context, error) =>
StreamMessageSearchListLoadMoreError(onTap: controller.retry),
StreamScrollViewLoadMoreError.list(
onTap: controller.retry,
error: Text(context.translations.loadingMessagesError),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamMessageSearchListLoadMoreIndicator(),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
ListView.separated(
padding: padding,
physics: physics,
reverse: reverse,
itemCount: 25,
separatorBuilder: (_, __) =>
const StreamMessageSearchListSeparator(),
itemBuilder: (_, __) => const StreamChannelListLoadingTile(),
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamMessageSearchListErrorWidget(
onPressed: controller.refresh,
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingMessagesError),
onRetryPressed: controller.refresh,
),
),
);
}
/// A [StreamMessageSearchListTile] that can be used in a [ListView] to show a
/// loading tile while waiting for the [StreamMessageSearchListController] to
/// load more messages.
class StreamMessageSearchListLoadMoreIndicator extends StatelessWidget {
/// Creates a new instance of [StreamMessageSearchListLoadMoreIndicator].
const StreamMessageSearchListLoadMoreIndicator({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator.adaptive(),
);
}
/// A [StreamMessageSearchListTile] that is used to display the error indicator
/// when loading more messages fails.
class StreamMessageSearchListLoadMoreError extends StatelessWidget {
/// Creates a new instance of [StreamMessageSearchListLoadMoreError].
const StreamMessageSearchListLoadMoreError({
Key? key,
this.onTap,
}) : super(key: key);
/// The callback to invoke when the user taps on the error indicator.
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context);
return InkWell(
onTap: onTap,
child: Container(
color: theme.colorTheme.textLowEmphasis.withOpacity(0.9),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.translations.loadingChannelsError,
style: theme.textTheme.body.copyWith(
color: Colors.white,
),
),
StreamSvgIcon.retry(color: Colors.white),
],
),
),
),
);
}
}
/// A widget that is used to display a separator between
/// [StreamMessageSearchListTile] items.
class StreamMessageSearchListSeparator extends StatelessWidget {
@@ -433,67 +384,3 @@ class StreamMessageSearchListSeparator extends StatelessWidget {
);
}
}
/// A widget that is used to display an error screen
/// when [StreamMessageSearchListController] fails to load initial messages.
class StreamMessageSearchListErrorWidget extends StatelessWidget {
/// Creates a new instance of [StreamMessageSearchListErrorWidget] widget.
const StreamMessageSearchListErrorWidget({
Key? key,
this.onPressed,
}) : super(key: key);
/// The callback to invoke when the user taps on the retry button.
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text.rich(
TextSpan(
children: [
const WidgetSpan(
child: Padding(
padding: EdgeInsets.only(right: 2),
child: Icon(Icons.error_outline),
),
),
TextSpan(text: context.translations.loadingChannelsError),
],
),
style: Theme.of(context).textTheme.headline6,
),
TextButton(
onPressed: onPressed,
child: Text(context.translations.retryLabel),
),
],
);
}
/// A widget that is used to display an empty state when
/// [StreamMessageSearchListController] loads zero messages.
class StreamMessageSearchListEmptyWidget extends StatelessWidget {
/// Creates a new instance of [StreamMessageSearchListEmptyWidget] widget.
const StreamMessageSearchListEmptyWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.message(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
const SizedBox(height: 28),
Text(
context.translations.letsStartChattingLabel,
style: chatThemeData.textTheme.headline,
),
],
);
}
}
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// A widget that shows an empty view when the [StreamScrollView] loads
/// empty data.
class StreamScrollViewEmptyWidget extends StatelessWidget {
/// Creates a new instance of the [StreamScrollViewEmptyWidget].
const StreamScrollViewEmptyWidget({
Key? key,
required this.emptyIcon,
required this.emptyTitle,
this.emptyTitleStyle,
this.mainAxisSize = MainAxisSize.max,
this.mainAxisAlignment = MainAxisAlignment.center,
this.crossAxisAlignment = CrossAxisAlignment.center,
}) : super(key: key);
/// The title of the empty view.
final Widget emptyTitle;
/// The style of the title.
final TextStyle? emptyTitleStyle;
/// The icon of the empty view.
final Widget emptyIcon;
/// The main axis size of the empty view.
final MainAxisSize mainAxisSize;
/// The main axis alignment of the empty view.
final MainAxisAlignment mainAxisAlignment;
/// The cross axis alignment of the empty view.
final CrossAxisAlignment crossAxisAlignment;
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
final emptyIcon = AnimatedSwitcher(
duration: kThemeChangeDuration,
child: this.emptyIcon,
);
final emptyTitleText = AnimatedDefaultTextStyle(
style: emptyTitleStyle ?? chatThemeData.textTheme.headline,
duration: kThemeChangeDuration,
child: emptyTitle,
);
return Column(
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: [
emptyIcon,
emptyTitleText,
],
);
}
}
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// A widget that is displayed when a [StreamScrollView] encounters an error
/// while loading the initial items.
class StreamScrollViewErrorWidget extends StatelessWidget {
/// Creates a new instance of the [StreamScrollViewErrorWidget].
const StreamScrollViewErrorWidget({
Key? key,
this.errorTitle,
this.errorTitleStyle,
this.errorIcon,
this.retryButtonText,
this.retryButtonTextStyle,
required this.onRetryPressed,
this.mainAxisSize = MainAxisSize.max,
this.mainAxisAlignment = MainAxisAlignment.center,
this.crossAxisAlignment = CrossAxisAlignment.center,
}) : super(key: key);
/// The title of the error.
final Widget? errorTitle;
/// The style of the title.
final TextStyle? errorTitleStyle;
/// The icon to display when the list shows error.
final Widget? errorIcon;
/// The text to display in the retry button.
final Widget? retryButtonText;
/// The style of the retryButtonText.
final TextStyle? retryButtonTextStyle;
/// The callback to invoke when the user taps on the retry button.
final VoidCallback onRetryPressed;
/// The main axis size of the error view.
final MainAxisSize mainAxisSize;
/// The main axis alignment of the error view.
final MainAxisAlignment mainAxisAlignment;
/// The cross axis alignment of the error view.
final CrossAxisAlignment crossAxisAlignment;
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
final errorIcon = AnimatedSwitcher(
duration: kThemeChangeDuration,
child: this.errorIcon ??
Icon(
Icons.error_outline_rounded,
size: 148,
color: chatThemeData.colorTheme.disabled,
),
);
final titleText = AnimatedDefaultTextStyle(
style: errorTitleStyle ?? chatThemeData.textTheme.headline,
duration: kThemeChangeDuration,
child: errorTitle ?? const SizedBox(),
);
final retryButtonText = AnimatedDefaultTextStyle(
style: errorTitleStyle ??
chatThemeData.textTheme.headline.copyWith(
color: Colors.white,
),
duration: kThemeChangeDuration,
child: this.retryButtonText ?? Text(context.translations.retryLabel),
);
return Column(
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: [
errorIcon,
titleText,
ElevatedButton(
onPressed: onRetryPressed,
child: retryButtonText,
),
],
);
}
}
@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
/// Signature for a function that creates a widget for a given index, e.g., in a
/// list, grid.
///
/// Used by [StreamChannelListView], [StreamMessageSearchListView]
/// and [StreamUserListView].
typedef StreamScrollViewIndexedWidgetBuilder<ItemType,
WidgetType extends Widget>
= Widget Function(
BuildContext context,
List<ItemType> items,
int index,
WidgetType defaultWidget,
);
@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
/// A tile that is used to display the error indicator when
/// loading more items fails.
class StreamScrollViewLoadMoreError extends StatelessWidget {
/// Creates a new instance of [StreamScrollViewLoadMoreError.list].
const StreamScrollViewLoadMoreError.list({
Key? key,
this.error,
this.errorStyle,
this.errorIcon,
this.backgroundColor,
required this.onTap,
this.padding = const EdgeInsets.all(16),
this.mainAxisSize = MainAxisSize.max,
this.mainAxisAlignment = MainAxisAlignment.spaceBetween,
this.crossAxisAlignment = CrossAxisAlignment.center,
}) : _isList = true,
super(key: key);
/// Creates a new instance of [StreamScrollViewLoadMoreError.grid].
const StreamScrollViewLoadMoreError.grid({
Key? key,
this.error,
this.errorStyle,
this.errorIcon,
this.backgroundColor,
required this.onTap,
this.padding = const EdgeInsets.all(16),
this.mainAxisSize = MainAxisSize.max,
this.mainAxisAlignment = MainAxisAlignment.spaceEvenly,
this.crossAxisAlignment = CrossAxisAlignment.center,
}) : _isList = false,
super(key: key);
/// The error message to display.
final Widget? error;
/// The style of the error message.
final TextStyle? errorStyle;
/// The icon to display next to the message.
final Widget? errorIcon;
/// The background color of the error message.
final Color? backgroundColor;
/// The callback to invoke when the user taps on the error indicator.
final GestureTapCallback onTap;
/// The amount of space by which to inset the child.
final EdgeInsetsGeometry padding;
/// The main axis size of the error view.
final MainAxisSize mainAxisSize;
/// The main axis alignment of the error view.
final MainAxisAlignment mainAxisAlignment;
/// The cross axis alignment of the error view.
final CrossAxisAlignment crossAxisAlignment;
final bool _isList;
@override
Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context);
final errorText = AnimatedDefaultTextStyle(
style: errorStyle ?? theme.textTheme.body.copyWith(color: Colors.white),
duration: kThemeChangeDuration,
child: error ?? const SizedBox(),
);
final errorIcon = AnimatedSwitcher(
duration: kThemeChangeDuration,
child: this.errorIcon ?? StreamSvgIcon.retry(color: Colors.white),
);
final backgroundColor = this.backgroundColor ??
theme.colorTheme.textLowEmphasis.withOpacity(0.9);
final children = [errorText, errorIcon];
return InkWell(
onTap: onTap,
child: Container(
color: backgroundColor,
child: Padding(
padding: padding,
child: _isList
? Row(
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: children,
)
: Column(
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: children,
),
),
),
);
}
}
@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
/// A widget that shows a loading indicator when the user is near the bottom of
/// the list.
class StreamScrollViewLoadMoreIndicator extends StatelessWidget {
/// Creates a new instance of [StreamScrollViewLoadMoreIndicator].
const StreamScrollViewLoadMoreIndicator({
Key? key,
this.height = 16,
this.width = 16,
}) : super(key: key);
/// The height of the indicator.
final double height;
/// The width of the indicator.
final double width;
@override
Widget build(BuildContext context) => SizedBox(
height: height,
width: width,
child: const CircularProgressIndicator.adaptive(),
);
}
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
/// A widget that is displayed while the [StreamScrollView] is loading.
class StreamScrollViewLoadingWidget extends StatelessWidget {
/// Creates a new instance of [StreamScrollViewLoadingWidget].
const StreamScrollViewLoadingWidget({
Key? key,
this.height = 42,
this.width = 42,
}) : super(key: key);
/// The height of the indicator.
final double height;
/// The width of the indicator.
final double width;
@override
Widget build(BuildContext context) => SizedBox(
height: height,
width: width,
child: const CircularProgressIndicator.adaptive(),
);
}
@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that displays a user.
///
/// This widget is intended to be used as a Tile in [StreamUserGridView]
///
/// It shows the user's avatar and name.
///
/// See also:
/// * [StreamUserGridView]
/// * [StreamUserAvatar]
class StreamUserGridTile extends StatelessWidget {
/// Creates a new instance of [StreamUserGridTile] widget.
const StreamUserGridTile({
Key? key,
required this.user,
this.child,
this.footer,
this.onTap,
this.onLongPress,
}) : super(key: key);
/// The user to display.
final User user;
/// The widget to display in the body of the tile.
final Widget? child;
/// The widget to display in the footer of the tile.
final Widget? footer;
/// Called when the user taps this grid tile.
final GestureTapCallback? onTap;
/// Called when the user long-presses on this grid tile.
final GestureLongPressCallback? onLongPress;
/// Creates a copy of this tile but with the given fields replaced with
/// the new values.
StreamUserGridTile copyWith({
Key? key,
User? user,
Widget? child,
Widget? footer,
GestureTapCallback? onTap,
GestureLongPressCallback? onLongPress,
}) =>
StreamUserGridTile(
key: key ?? this.key,
user: user ?? this.user,
footer: footer ?? this.footer,
onTap: onTap ?? this.onTap,
onLongPress: onLongPress ?? this.onLongPress,
child: child ?? this.child,
);
@override
Widget build(BuildContext context) {
final child = this.child ??
StreamUserAvatar(
user: user,
borderRadius: BorderRadius.circular(32),
constraints: const BoxConstraints.tightFor(
height: 64,
width: 64,
),
onlineIndicatorConstraints: const BoxConstraints.tightFor(
height: 12,
width: 12,
),
);
final footer = this.footer ??
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
user.name,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
);
return InkWell(
onTap: onTap,
onLongPress: onLongPress,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
child,
const SizedBox(height: 4),
footer,
],
),
);
}
}
@@ -0,0 +1,394 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default grid delegate for [StreamUserGridView].
const defaultUserGridViewDelegate =
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4);
/// Signature for the item builder that creates the children of the
/// [StreamUserGridView].
typedef StreamUserGridViewIndexedWidgetBuilder
= StreamScrollViewIndexedWidgetBuilder<User, StreamUserGridTile>;
/// A [GridView] that shows a grid of [User]s,
/// it uses [StreamUserGridTile] as a default item.
///
/// Example:
///
/// ```dart
/// StreamUserGridView(
/// controller: controller,
/// onUserTap: (user) {
/// // Handle user tap event
/// },
/// onUserLongPress: (user) {
/// // Handle user long press event
/// },
/// )
/// ```
///
/// See also:
/// * [StreamUserListTile]
/// * [StreamUserListController]
class StreamUserGridView extends StatelessWidget {
/// Creates a new instance of [StreamUserGridView].
const StreamUserGridView({
Key? key,
required this.controller,
this.gridDelegate = defaultUserGridViewDelegate,
this.itemBuilder,
this.emptyBuilder,
this.loadMoreErrorBuilder,
this.loadMoreIndicatorBuilder,
this.loadingBuilder,
this.errorBuilder,
this.onUserTap,
this.onUserLongPress,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
}) : super(key: key);
/// The [StreamUserListController] used to control the grid of users.
final StreamUserListController controller;
/// A delegate that controls the layout of the children within
/// the [PagedValueGridView].
final SliverGridDelegate gridDelegate;
/// A builder that is called to build items in the [PagedValueGridView].
final StreamUserGridViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the empty state of the grid.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the load more error state of the grid.
final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder;
/// A builder that is called to build the load more indicator of the grid.
final WidgetBuilder? loadMoreIndicatorBuilder;
/// A builder that is called to build the loading state of the grid.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the grid.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the user taps this grid tile.
final void Function(User)? onUserTap;
/// Called when the user long-presses on this grid tile.
final void Function(User)? onUserLongPress;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by
/// an otherwise focused part of the application,
/// the ScrollAction will be evaluated using this scroll view, for example,
/// when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// The number of children that will contribute semantic information.
///
/// Some subtypes of [ScrollView] can infer this value automatically. For
/// example [ListView] will use the number of widgets in the child list,
/// while the [ListView.separated] constructor will use half that amount.
///
/// For [CustomScrollView] and other types which do not receive a builder
/// or list of widgets, the child count must be explicitly provided. If the
/// number is unknown or unbounded this should be left unset or set to null.
///
/// See also:
///
/// * [SemanticsConfiguration.scrollChildCount],
/// the corresponding semantics property.
final int? semanticChildCount;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
Widget build(BuildContext context) {
return PagedValueGridView<int, User>(
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
scrollController: scrollController,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
clipBehavior: clipBehavior,
gridDelegate: gridDelegate,
itemBuilder: (context, users, index) {
final user = users[index];
final onTap = onUserTap;
final onLongPress = onUserLongPress;
final streamUserGridTile = StreamUserGridTile(
user: user,
onTap: onTap == null ? null : () => onTap(user),
onLongPress: onLongPress == null ? null : () => onLongPress(user),
);
return itemBuilder?.call(
context,
users,
index,
streamUserGridTile,
) ??
streamUserGridTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.user(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.noUsersLabel,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamScrollViewLoadMoreError.grid(
onTap: controller.retry,
error: Text(
context.translations.loadingUsersError,
textAlign: TextAlign.center,
),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingUsersError),
onRetryPressed: controller.refresh,
),
),
);
}
}
@@ -48,10 +48,11 @@ class StreamUserListTile extends StatelessWidget {
/// A widget to display at the end of tile.
final Widget? selectedWidget;
/// If this tile is also [enabled] then icons and text are rendered with the same color.
/// If this tile is also [enabled] then icons
/// and text are rendered with the same color.
///
/// By default the selected color is the theme's primary color. The selected color
/// can be overridden with a [ListTileTheme].
/// By default the selected color is the theme's primary color.
/// The selected color can be overridden with a [ListTileTheme].
///
/// {@tool dartpad}
/// Here is an example of using a [StatefulWidget] to keep track of the
@@ -1,6 +1,12 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default separator builder for [StreamUserListView].
@@ -14,7 +20,7 @@ Widget defaultUserListViewSeparatorBuilder(
/// Signature for the item builder that creates the children of the
/// [StreamUserListView].
typedef StreamUserListViewIndexedWidgetBuilder
= StreamListViewIndexedWidgetBuilder<User, StreamUserListTile>;
= StreamScrollViewIndexedWidgetBuilder<User, StreamUserListTile>;
/// A [ListView] that shows a list of [User]s,
/// it uses [StreamUserListTile] as a default item.
@@ -73,28 +79,18 @@ class StreamUserListView extends StatelessWidget {
final StreamUserListController controller;
/// A builder that is called to build items in the [ListView].
///
/// The `user` parameter is the [User] at this position in the list
/// and the `defaultWidget` is the default widget used
/// i.e: [StreamUserListTile].
final StreamUserListViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the list separator.
final PagedValueScrollViewIndexedWidgetBuilder<User> separatorBuilder;
/// A builder that is called to build the empty state of the list.
///
/// If not provided, [StreamUserListEmptyWidget] will be used.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the loading state of the list.
///
/// If not provided, [StreamUserListLoadingTile] will be used.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the list.
///
/// If not provided, [StreamUserListErrorWidget] will be used.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the user taps this list tile.
@@ -322,96 +318,52 @@ class StreamUserListView extends StatelessWidget {
) ??
streamUserListTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.user(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.noUsersLabel,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamUserListLoadMoreError(onTap: controller.retry),
StreamScrollViewLoadMoreError.list(
onTap: controller.retry,
error: Text(context.translations.loadingUsersError),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamUserListLoadMoreIndicator(),
child: StreamScrollViewLoadMoreIndicator(),
),
),
emptyBuilder: (context) =>
emptyBuilder?.call(context) ??
const Center(
child: Padding(
padding: EdgeInsets.all(8),
child: StreamUserListEmptyWidget(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
ListView.separated(
padding: padding,
physics: physics,
reverse: reverse,
itemCount: 25,
separatorBuilder: (_, __) => const StreamUserListSeparator(),
itemBuilder: (_, __) => const StreamChannelListLoadingTile(),
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamUserListErrorWidget(
onPressed: controller.refresh,
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingUsersError),
onRetryPressed: controller.refresh,
),
),
);
}
/// A [StreamUserListTile] that can be used in a [ListView] to show a
/// loading tile while waiting for the [StreamUserListController] to load
/// more channels.
class StreamUserListLoadMoreIndicator extends StatelessWidget {
/// Creates a new instance of [StreamUserListLoadMoreIndicator].
const StreamUserListLoadMoreIndicator({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator.adaptive(),
);
}
/// A [StreamUserListTile] that is used to display the error indicator when
/// loading more users fails.
class StreamUserListLoadMoreError extends StatelessWidget {
/// Creates a new instance of [StreamUserListLoadMoreError].
const StreamUserListLoadMoreError({
Key? key,
this.onTap,
}) : super(key: key);
/// The callback to invoke when the user taps on the error indicator.
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context);
return InkWell(
onTap: onTap,
child: Container(
color: theme.colorTheme.textLowEmphasis.withOpacity(0.9),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.translations.loadingChannelsError,
style: theme.textTheme.body.copyWith(
color: Colors.white,
),
),
StreamSvgIcon.retry(color: Colors.white),
],
),
),
),
);
}
}
/// A widget that is used to display a separator between
/// [StreamUserListTile] items.
class StreamUserListSeparator extends StatelessWidget {
@@ -427,67 +379,3 @@ class StreamUserListSeparator extends StatelessWidget {
);
}
}
/// A widget that is used to display an error screen
/// when [StreamUserListController] fails to load initial users.
class StreamUserListErrorWidget extends StatelessWidget {
/// Creates a new instance of [StreamUserListErrorWidget] widget.
const StreamUserListErrorWidget({
Key? key,
this.onPressed,
}) : super(key: key);
/// The callback to invoke when the user taps on the retry button.
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text.rich(
TextSpan(
children: [
const WidgetSpan(
child: Padding(
padding: EdgeInsets.only(right: 2),
child: Icon(Icons.error_outline),
),
),
TextSpan(text: context.translations.loadingChannelsError),
],
),
style: Theme.of(context).textTheme.headline6,
),
TextButton(
onPressed: onPressed,
child: Text(context.translations.retryLabel),
),
],
);
}
/// A widget that is used to display an empty state when
/// [StreamUserListController] loads zero users.
class StreamUserListEmptyWidget extends StatelessWidget {
/// Creates a new instance of [StreamUserListEmptyWidget] widget.
const StreamUserListEmptyWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.message(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
const SizedBox(height: 28),
Text(
context.translations.letsStartChattingLabel,
style: chatThemeData.textTheme.headline,
),
],
);
}
}
@@ -1,9 +0,0 @@
import 'package:flutter/material.dart';
typedef StreamListViewIndexedWidgetBuilder<ItemType, WidgetType extends Widget>
= Widget Function(
BuildContext context,
List<ItemType> items,
int index,
WidgetType defaultWidget,
);
@@ -22,6 +22,7 @@ export 'src/info_tile.dart';
export 'src/localization/stream_chat_localizations.dart';
export 'src/localization/translations.dart' show DefaultTranslations;
export 'src/message_action.dart';
// ignore: deprecated_member_use_from_same_package
export 'src/message_input.dart' show MessageInput, MessageInputState;
export 'src/message_list_view.dart';
export 'src/message_search_item.dart';
@@ -48,23 +49,29 @@ export 'src/user_item.dart';
export 'src/user_list_view.dart';
export 'src/user_mention_tile.dart';
export 'src/utils.dart';
// v4
export 'src/v4/channel_list_view/stream_channel_list_loading_tile.dart';
export 'src/v4/channel_list_view/stream_channel_list_tile.dart';
export 'src/v4/channel_list_view/stream_channel_list_view.dart';
export 'src/v4/message_input/countdown_button.dart';
export 'src/v4/message_input/stream_attachment_picker.dart';
export 'src/v4/message_input/stream_message_input.dart';
export 'src/v4/message_input/stream_message_send_button.dart';
export 'src/v4/message_input/stream_message_text_field.dart';
export 'src/v4/message_search_list_view/stream_message_search_list_tile.dart';
export 'src/v4/message_search_list_view/stream_message_search_list_view.dart';
export 'src/v4/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart';
export 'src/v4/scroll_view/channel_scroll_view/stream_channel_grid_view.dart';
export 'src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart';
export 'src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart';
export 'src/v4/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart';
export 'src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart';
export 'src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart';
export 'src/v4/scroll_view/stream_scroll_view_empty_widget.dart';
export 'src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart';
export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart';
export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart';
export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart';
export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart';
export 'src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart';
export 'src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart';
export 'src/v4/stream_channel_avatar.dart';
export 'src/v4/stream_channel_info_bottom_sheet.dart';
export 'src/v4/stream_channel_name.dart';
export 'src/v4/stream_list_view_indexed_widget_builder.dart';
export 'src/v4/stream_message_preview_text.dart';
export 'src/v4/user_list_view/stream_user_list_tile.dart';
export 'src/v4/user_list_view/stream_user_list_view.dart';
export 'src/visible_footnote.dart';
@@ -280,7 +280,7 @@ void main() {
expect(find.text('test'), findsNothing);
expect(find.byType(StreamBackButton), findsNothing);
expect(find.byType(ChannelAvatar), findsNothing);
expect(find.byType(StreamChannelAvatar), findsNothing);
expect(find.byType(StreamChannelInfo), findsNothing);
expect(find.text('leading'), findsOneWidget);
expect(find.text('title'), findsOneWidget);
@@ -31,8 +31,10 @@ void main() {
client: client,
child: StreamChannel(
channel: channel,
child: const Scaffold(
body: ChannelAvatar(),
child: Scaffold(
body: StreamChannelAvatar(
channel: channel,
),
),
),
),
@@ -101,8 +103,10 @@ void main() {
client: client,
child: StreamChannel(
channel: channel,
child: const Scaffold(
body: ChannelAvatar(),
child: Scaffold(
body: StreamChannelAvatar(
channel: channel,
),
),
),
),
@@ -162,8 +166,10 @@ void main() {
client: client,
child: StreamChannel(
channel: channel,
child: const Scaffold(
body: ChannelAvatar(),
child: Scaffold(
body: StreamChannelAvatar(
channel: channel,
),
),
),
),
@@ -202,9 +208,10 @@ void main() {
client: client,
child: StreamChannel(
channel: channel,
child: const Scaffold(
body: ChannelAvatar(
child: Scaffold(
body: StreamChannelAvatar(
selected: true,
channel: channel,
),
),
),
@@ -57,8 +57,10 @@ void main() {
client: client,
child: StreamChannel(
channel: channel,
child: const Scaffold(
body: ChannelName(),
child: Scaffold(
body: StreamChannelName(
channel: channel,
),
),
),
),
@@ -1,3 +1,5 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@@ -0,0 +1,37 @@
import 'package:stream_chat_flutter/src/media_list_view_controller.dart';
import 'package:test/test.dart';
void main() {
test('should update media', () {
final controller = MediaListViewController();
expect(controller.shouldUpdateMedia, false);
controller.updateMedia(newValue: true);
expect(controller.shouldUpdateMedia, true);
controller.dispose();
});
test('should notify listeners on update media', () {
final controller = MediaListViewController();
var callCount = 0;
void updateCallsSpy() => callCount++;
controller.addListener(updateCallsSpy);
expect(callCount, 0);
controller.updateMedia(newValue: false);
expect(controller.shouldUpdateMedia, false);
expect(callCount, 1);
controller.updateMedia(newValue: true);
expect(controller.shouldUpdateMedia, true);
expect(callCount, 2);
controller
..removeListener(updateCallsSpy)
..dispose();
});
}
@@ -1,3 +1,5 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -1,3 +1,6 @@
// ignore: lines_longer_than_80_chars
// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -1,3 +1,6 @@
// ignore: lines_longer_than_80_chars
// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -1,3 +1,5 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -6,7 +6,7 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:name="${applicationName}"
android:label="example"
android:icon="@mipmap/ic_launcher">
<activity
@@ -0,0 +1,6 @@
package example.example
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -60,102 +60,114 @@ class StreamExample extends StatelessWidget {
}
/// Basic layout displaying a list of [Channel]s the user is a part of.
/// This is implemented using [ChannelListCore].
/// This is implemented using a [StreamChannelListController].
///
/// [ChannelListCore] is a `builder` with callbacks for constructing UIs based
/// on different scenarios.
class HomeScreen extends StatelessWidget {
/// [StreamChannelListController] is a controller that lets you manage a list of
/// channels.
class HomeScreen extends StatefulWidget {
/// Builds a basic layout displaying a list of [Channel]s the user is a
/// part of.
HomeScreen({Key? key}) : super(key: key);
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
/// Controller used for loading more data and controlling pagination in
/// [ChannelListCore].
final channelListController = ChannelListController();
/// [StreamChannelListController].
late final channelListController = StreamChannelListController(
client: StreamChatCore.of(context).client,
filter: Filter.and([
Filter.equal('type', 'messaging'),
Filter.in_(
'members',
[
StreamChatCore.of(context).currentUser!.id,
],
),
]),
);
@override
void initState() {
channelListController.doInitialLoad();
super.initState();
}
@override
void dispose() {
channelListController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Channels'),
),
body: ChannelsBloc(
child: ChannelListCore(
channelListController: channelListController,
filter: Filter.and([
Filter.equal('type', 'messaging'),
Filter.in_(
'members',
[
StreamChatCore.of(context).currentUser!.id,
],
),
]),
emptyBuilder: (BuildContext context) => const Center(
child: Text('Looks like you are not in any channels'),
),
loadingBuilder: (BuildContext context) => const Center(
child: SizedBox(
height: 100,
width: 100,
child: CircularProgressIndicator(),
),
),
errorBuilder: (
BuildContext context,
dynamic error,
) =>
Center(
child: Text(
'Oh no, something went wrong. '
'Please check your config. $error',
),
),
listBuilder: (
BuildContext context,
List<Channel> channels,
) =>
LazyLoadScrollView(
onEndOfPage: () async {
channelListController.paginateData!();
},
child: ListView.builder(
itemCount: channels.length,
itemBuilder: (BuildContext context, int index) {
final _item = channels[index];
return ListTile(
title: Text(_item.name ?? ''),
subtitle: StreamBuilder<Message?>(
stream: _item.state!.lastMessageStream,
initialData: _item.state!.lastMessage,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.text!);
}
return const SizedBox();
},
),
onTap: () {
/// Display a list of messages when the user taps on
/// an item. We can use [StreamChannel] to wrap our
/// [MessageScreen] screen with the selected channel.
///
/// This allows us to use a built-in inherited widget
/// for accessing our `channel` later on.
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: _item,
child: const MessageScreen(),
),
),
);
},
);
body: PagedValueListenableBuilder<int, Channel>(
valueListenable: channelListController,
builder: (context, value, child) {
return value.when(
(channels, nextPageKey, error) => LazyLoadScrollView(
onEndOfPage: () async {
if (nextPageKey != null) {
channelListController.loadMore(nextPageKey);
}
},
child: ListView.builder(
itemCount: channels.length,
itemBuilder: (BuildContext context, int index) {
final _item = channels[index];
return ListTile(
title: Text(_item.name ?? ''),
subtitle: StreamBuilder<Message?>(
stream: _item.state!.lastMessageStream,
initialData: _item.state!.lastMessage,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.text!);
}
return const SizedBox();
},
),
onTap: () {
/// Display a list of messages when the user taps on
/// an item. We can use [StreamChannel] to wrap our
/// [MessageScreen] screen with the selected channel.
///
/// This allows us to use a built-in inherited widget
/// for accessing our `channel` later on.
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: _item,
child: const MessageScreen(),
),
),
);
},
);
},
),
),
),
),
loading: () => const Center(
child: SizedBox(
height: 100,
width: 100,
child: CircularProgressIndicator(),
),
),
error: (e) => Center(
child: Text(
'Oh no, something went wrong. '
'Please check your config. $e',
),
),
);
},
),
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="example">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>example</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
var serviceWorkerVersion = null;
var scriptLoaded = false;
function loadMainDartJs() {
if (scriptLoaded) {
return;
}
scriptLoaded = true;
var scriptTag = document.createElement('script');
scriptTag.src = 'main.dart.js';
scriptTag.type = 'application/javascript';
document.body.append(scriptTag);
}
if ('serviceWorker' in navigator) {
// Service workers are supported. Use them.
window.addEventListener('load', function () {
// Wait for registration to finish before dropping the <script> tag.
// Otherwise, the browser will load the script multiple times,
// potentially different versions.
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
navigator.serviceWorker.register(serviceWorkerUrl)
.then((reg) => {
function waitForActivation(serviceWorker) {
serviceWorker.addEventListener('statechange', () => {
if (serviceWorker.state == 'activated') {
console.log('Installed new service worker.');
loadMainDartJs();
}
});
}
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing || reg.waiting);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log('New service worker available.');
reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log('Loading app from service worker.');
loadMainDartJs();
}
});
// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
'Failed to load app from service worker. Falling back to plain <script> tag.',
);
loadMainDartJs();
}
}, 4000);
});
} else {
// Service workers not supported. Just drop the <script> tag.
loadMainDartJs();
}
</script>
</body>
</html>
@@ -0,0 +1,35 @@
{
"name": "example",
"short_name": "example",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
@@ -0,0 +1,17 @@
flutter/ephemeral/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
@@ -0,0 +1,95 @@
cmake_minimum_required(VERSION 3.14)
project(example LANGUAGES CXX)
set(BINARY_NAME "example")
cmake_policy(SET CMP0063 NEW)
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Configure build options.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
else()
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
endif()
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
# Use Unicode for all projects.
add_definitions(-DUNICODE -D_UNICODE)
# Compilation settings that should be applied to most targets.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
# Flutter library and tool build rules.
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)
@@ -0,0 +1,103 @@
cmake_minimum_required(VERSION 3.14)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_windows.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
"flutter_texture_registrar.h"
)
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${PHONY_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
windows-x64 $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)
@@ -0,0 +1,14 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <connectivity_plus_windows/connectivity_plus_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
}
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
@@ -0,0 +1,16 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus_windows
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
@@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.14)
project(runner LANGUAGES CXX)
add_executable(${BINARY_NAME} WIN32
"flutter_window.cpp"
"main.cpp"
"utils.cpp"
"win32_window.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc"
"runner.exe.manifest"
)
apply_standard_settings(${BINARY_NAME})
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
add_dependencies(${BINARY_NAME} flutter_assemble)
@@ -0,0 +1,121 @@
// Microsoft Visual C++ generated resource script.
//
#pragma code_page(65001)
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico"
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
#ifdef FLUTTER_BUILD_NUMBER
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
#else
#define VERSION_AS_NUMBER 1,0,0
#endif
#ifdef FLUTTER_BUILD_NAME
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
#else
#define VERSION_AS_STRING "1.0.0"
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_AS_NUMBER
PRODUCTVERSION VERSION_AS_NUMBER
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "example" "\0"
VALUE "FileDescription", "example" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "example" "\0"
VALUE "LegalCopyright", "Copyright (C) 2022 example. All rights reserved." "\0"
VALUE "OriginalFilename", "example.exe" "\0"
VALUE "ProductName", "example" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED
@@ -0,0 +1,61 @@
#include "flutter_window.h"
#include <optional>
#include "flutter/generated_plugin_registrant.h"
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
FlutterWindow::~FlutterWindow() {}
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
// The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false;
}
RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow());
return true;
}
void FlutterWindow::OnDestroy() {
if (flutter_controller_) {
flutter_controller_ = nullptr;
}
Win32Window::OnDestroy();
}
LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
// Give Flutter, including plugins, an opportunity to handle window messages.
if (flutter_controller_) {
std::optional<LRESULT> result =
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (result) {
return *result;
}
}
switch (message) {
case WM_FONTCHANGE:
flutter_controller_->engine()->ReloadSystemFonts();
break;
}
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}
@@ -0,0 +1,33 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#include "win32_window.h"
// A window that does nothing but host a Flutter view.
class FlutterWindow : public Win32Window {
public:
// Creates a new FlutterWindow hosting a Flutter view running |project|.
explicit FlutterWindow(const flutter::DartProject& project);
virtual ~FlutterWindow();
protected:
// Win32Window:
bool OnCreate() override;
void OnDestroy() override;
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
LPARAM const lparam) noexcept override;
private:
// The project to run.
flutter::DartProject project_;
// The Flutter instance hosted by this window.
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_
@@ -0,0 +1,43 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#include "flutter_window.h"
#include "utils.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
CreateAndAttachConsole();
}
// Initialize COM, so that it is available for use in the library and/or
// plugins.
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
flutter::DartProject project(L"data");
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.CreateAndShow(L"example", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
::CoUninitialize();
return EXIT_SUCCESS;
}
@@ -0,0 +1,16 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Runner.rc
//
#define IDI_APP_ICON 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
</application>
</compatibility>
</assembly>
@@ -0,0 +1,64 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
void CreateAndAttachConsole() {
if (::AllocConsole()) {
FILE *unused;
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
_dup2(_fileno(stdout), 1);
}
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
_dup2(_fileno(stdout), 2);
}
std::ios::sync_with_stdio();
FlutterDesktopResyncOutputStreams();
}
}
std::vector<std::string> GetCommandLineArguments() {
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
int argc;
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
if (argv == nullptr) {
return std::vector<std::string>();
}
std::vector<std::string> command_line_arguments;
// Skip the first argument as it's the binary name.
for (int i = 1; i < argc; i++) {
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
}
::LocalFree(argv);
return command_line_arguments;
}
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
if (utf16_string == nullptr) {
return std::string();
}
int target_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, nullptr, 0, nullptr, nullptr);
if (target_length == 0) {
return std::string();
}
std::string utf8_string;
utf8_string.resize(target_length);
int converted_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, utf8_string.data(),
target_length, nullptr, nullptr);
if (converted_length == 0) {
return std::string();
}
return utf8_string;
}
@@ -0,0 +1,19 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// Creates a console for the process, and redirects stdout and stderr to
// it for both the runner and the Flutter library.
void CreateAndAttachConsole();
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
// encoded in UTF-8. Returns an empty std::string on failure.
std::string Utf8FromUtf16(const wchar_t* utf16_string);
// Gets the command line arguments passed in as a std::vector<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_
@@ -0,0 +1,245 @@
#include "win32_window.h"
#include <flutter_windows.h>
#include "resource.h"
namespace {
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in
// scale factor
int Scale(int source, double scale_factor) {
return static_cast<int>(source * scale_factor);
}
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
// This API is only needed for PerMonitor V1 awareness mode.
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
HMODULE user32_module = LoadLibraryA("User32.dll");
if (!user32_module) {
return;
}
auto enable_non_client_dpi_scaling =
reinterpret_cast<EnableNonClientDpiScaling*>(
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
if (enable_non_client_dpi_scaling != nullptr) {
enable_non_client_dpi_scaling(hwnd);
FreeLibrary(user32_module);
}
}
} // namespace
// Manages the Win32Window's window class registration.
class WindowClassRegistrar {
public:
~WindowClassRegistrar() = default;
// Returns the singleton registar instance.
static WindowClassRegistrar* GetInstance() {
if (!instance_) {
instance_ = new WindowClassRegistrar();
}
return instance_;
}
// Returns the name of the window class, registering the class if it hasn't
// previously been registered.
const wchar_t* GetWindowClass();
// Unregisters the window class. Should only be called if there are no
// instances of the window.
void UnregisterWindowClass();
private:
WindowClassRegistrar() = default;
static WindowClassRegistrar* instance_;
bool class_registered_ = false;
};
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
const wchar_t* WindowClassRegistrar::GetWindowClass() {
if (!class_registered_) {
WNDCLASS window_class{};
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon =
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc;
RegisterClass(&window_class);
class_registered_ = true;
}
return kWindowClassName;
}
void WindowClassRegistrar::UnregisterWindowClass() {
UnregisterClass(kWindowClassName, nullptr);
class_registered_ = false;
}
Win32Window::Win32Window() {
++g_active_window_count;
}
Win32Window::~Win32Window() {
--g_active_window_count;
Destroy();
}
bool Win32Window::CreateAndShow(const std::wstring& title,
const Point& origin,
const Size& size) {
Destroy();
const wchar_t* window_class =
WindowClassRegistrar::GetInstance()->GetWindowClass();
const POINT target_point = {static_cast<LONG>(origin.x),
static_cast<LONG>(origin.y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);
if (!window) {
return false;
}
return OnCreate();
}
// static
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
if (message == WM_NCCREATE) {
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window;
} else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam);
}
return DefWindowProc(window, message, wparam, lparam);
}
LRESULT
Win32Window::MessageHandler(HWND hwnd,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) {
case WM_DESTROY:
window_handle_ = nullptr;
Destroy();
if (quit_on_close_) {
PostQuitMessage(0);
}
return 0;
case WM_DPICHANGED: {
auto newRectSize = reinterpret_cast<RECT*>(lparam);
LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_SIZE: {
RECT rect = GetClientArea();
if (child_content_ != nullptr) {
// Size and position the child window.
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, TRUE);
}
return 0;
}
case WM_ACTIVATE:
if (child_content_ != nullptr) {
SetFocus(child_content_);
}
return 0;
}
return DefWindowProc(window_handle_, message, wparam, lparam);
}
void Win32Window::Destroy() {
OnDestroy();
if (window_handle_) {
DestroyWindow(window_handle_);
window_handle_ = nullptr;
}
if (g_active_window_count == 0) {
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
}
}
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
return reinterpret_cast<Win32Window*>(
GetWindowLongPtr(window, GWLP_USERDATA));
}
void Win32Window::SetChildContent(HWND content) {
child_content_ = content;
SetParent(content, window_handle_);
RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true);
SetFocus(child_content_);
}
RECT Win32Window::GetClientArea() {
RECT frame;
GetClientRect(window_handle_, &frame);
return frame;
}
HWND Win32Window::GetHandle() {
return window_handle_;
}
void Win32Window::SetQuitOnClose(bool quit_on_close) {
quit_on_close_ = quit_on_close;
}
bool Win32Window::OnCreate() {
// No-op; provided for subclasses.
return true;
}
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}
@@ -0,0 +1,98 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
// inherited from by classes that wish to specialize with custom
// rendering and input handling
class Win32Window {
public:
struct Point {
unsigned int x;
unsigned int y;
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
};
struct Size {
unsigned int width;
unsigned int height;
Size(unsigned int width, unsigned int height)
: width(width), height(height) {}
};
Win32Window();
virtual ~Win32Window();
// Creates and shows a win32 window with |title| and position and size using
// |origin| and |size|. New windows are created on the default monitor. Window
// sizes are specified to the OS in physical pixels, hence to ensure a
// consistent size to will treat the width height passed in to this function
// as logical pixels and scale to appropriate for the default monitor. Returns
// true if the window was created successfully.
bool CreateAndShow(const std::wstring& title,
const Point& origin,
const Size& size);
// Release OS resources associated with window.
void Destroy();
// Inserts |content| into the window tree.
void SetChildContent(HWND content);
// Returns the backing Window handle to enable clients to set icon and other
// window properties. Returns nullptr if the window has been destroyed.
HWND GetHandle();
// If true, closing this window will quit the application.
void SetQuitOnClose(bool quit_on_close);
// Return a RECT representing the bounds of the current client area.
RECT GetClientArea();
protected:
// Processes and route salient window messages for mouse handling,
// size change and DPI. Delegates handling of these to member overloads that
// inheriting classes can handle.
virtual LRESULT MessageHandler(HWND window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Called when CreateAndShow is called, allowing subclass window-related
// setup. Subclasses should return false if setup fails.
virtual bool OnCreate();
// Called when Destroy is called.
virtual void OnDestroy();
private:
friend class WindowClassRegistrar;
// OS callback called by message pump. Handles the WM_NCCREATE message which
// is passed when the non-client area is being created and enables automatic
// non-client DPI scaling so that the non-client area automatically
// responsponds to changes in DPI. All other messages are handled by
// MessageHandler.
static LRESULT CALLBACK WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
bool quit_on_close_ = false;
// window handle for top level window.
HWND window_handle_ = nullptr;
// window handle for hosted content.
HWND child_content_ = nullptr;
};
#endif // RUNNER_WIN32_WINDOW_H_
@@ -1,3 +1,5 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:async';
import 'dart:convert';
@@ -54,6 +56,11 @@ import 'package:stream_chat_flutter_core/src/typedef.dart';
///
/// Make sure to have a [StreamChatCore] ancestor in order to provide the
/// information about the channels.
@Deprecated('''
ChannelListCore is deprecated and will be removed in the next
major version. Use StreamChannelListController instead to create your custom list.
More details here https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter_core/stream_channel_list_controller
''')
class ChannelListCore extends StatefulWidget {
/// Instantiate a new ChannelListView
const ChannelListCore({

Some files were not shown because too many files have changed in this diff Show More