Merge branch 'develop'

This commit is contained in:
Salvatore Giordano
2022-09-30 17:56:15 +02:00
556 changed files with 29674 additions and 135869 deletions
+2 -2
View File
@@ -1,9 +1,9 @@
name: Dart Code Metrics
env:
flutter_version: "3.0.0"
flutter_version: "3.3.3"
folders: "lib, test"
melos_version: "2.1.0"
melos_version: "2.7.1"
on:
pull_request:
+12 -7
View File
@@ -2,8 +2,8 @@ name: stream_flutter_workflow
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
flutter_version: "3.0.0"
melos_version: "2.1.0"
flutter_version: "3.3.3"
melos_version: "2.7.1"
on:
pull_request:
@@ -77,7 +77,7 @@ jobs:
test:
runs-on: macos-latest
if: github.event.pull_request.draft == false
timeout-minutes: 20
timeout-minutes: 30
steps:
- name: "Git Checkout"
uses: actions/checkout@v2
@@ -111,22 +111,27 @@ jobs:
uses: VeryGoodOpenSource/very_good_coverage@v1.1.1
with:
path: packages/stream_chat/coverage/lcov.info
min_coverage: 80
min_coverage: 79
- name: "Stream Chat Persistence Coverage Check"
uses: VeryGoodOpenSource/very_good_coverage@v1.1.1
with:
path: packages/stream_chat_localizations/coverage/lcov.info
min_coverage: 88
- name: "Stream Chat Persistence Coverage Check"
uses: VeryGoodOpenSource/very_good_coverage@v1.1.1
with:
path: packages/stream_chat_persistence/coverage/lcov.info
min_coverage: 95
min_coverage: 97
- name: "Stream Chat Flutter Core Coverage Check"
uses: VeryGoodOpenSource/very_good_coverage@v1.1.1
with:
path: packages/stream_chat_flutter_core/coverage/lcov.info
min_coverage: 90
min_coverage: 30
- name: "Stream Chat Flutter Coverage Check"
uses: VeryGoodOpenSource/very_good_coverage@v1.1.1
with:
path: packages/stream_chat_flutter/coverage/lcov.info
min_coverage: 67
min_coverage: 44
draft-build:
runs-on: ubuntu-latest
-1
View File
@@ -198,7 +198,6 @@ analyzer:
exclude:
- packages/*/lib/**/*.g.dart
- packages/*/example/**
- packages/*/lib/src/emoji
- packages/*/lib/**/*.freezed.dart
- packages/*/test/**
-1
View File
@@ -3,7 +3,6 @@ analyzer:
- dart_code_metrics
exclude:
- packages/*/lib/**/*.g.dart
- packages/*/lib/src/emoji/**
- packages/*/lib/scrollable_positioned_list/**
- packages/*/lib/**/*.freezed.dart
@@ -0,0 +1,118 @@
---
id: autocomplete_triggers
title: Adding Custom Autocomplete Triggers
---
Adding Custom Autocomplete Triggers
### Introduction
The [StreamMessageInput](../stream_chat_flutter/message_input.mdx) widget provides a way to add custom autocomplete triggers using the `StreamMessageInput.customAutocompleteTriggers` property.
By default we provide autocomplete triggers for mentions and commands, but it's very easy to add your custom ones.
### Add Emoji Autocomplete Trigger
To add a custom emoji autocomplete trigger, you must first create an `AutoCompleteOptions` widget.
This widget will be used to show the autocomplete options.
For this example we're using two external dependencies:
- [emojis](https://pub.dev/packages/emojis)
- [substring_highlight](https://pub.dev/packages/substring_highlight)
```dart
import 'package:emojis/emoji.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:substring_highlight/substring_highlight.dart';
/// Overlay for displaying emoji that can be used
class StreamEmojiAutocompleteOptions extends StatelessWidget {
/// Constructor for creating a [StreamEmojiAutocompleteOptions]
const StreamEmojiAutocompleteOptions({
super.key,
required this.query,
this.onEmojiSelected,
});
/// Query for searching emoji.
final String query;
/// Callback called when an emoji is selected.
final ValueSetter<Emoji>? onEmojiSelected;
@override
Widget build(BuildContext context) {
final emojis = Emoji.all().where((it) {
final normalizedQuery = query.toUpperCase();
final normalizedShortName = it.shortName.toUpperCase();
return normalizedShortName.contains(normalizedQuery);
});
if (emojis.isEmpty) return const SizedBox.shrink();
return StreamAutocompleteOptions<Emoji>(
options: emojis,
optionBuilder: (context, emoji) {
final themeData = Theme.of(context);
return ListTile(
dense: true,
horizontalTitleGap: 0,
leading: Text(
emoji.char,
style: themeData.textTheme.headline6!.copyWith(
fontSize: 24,
),
),
title: SubstringHighlight(
text: emoji.shortName,
term: query,
textStyleHighlight: themeData.textTheme.headline6!.copyWith(
color: Colors.yellow,
fontSize: 14.5,
fontWeight: FontWeight.bold,
),
textStyle: themeData.textTheme.headline6!.copyWith(
fontSize: 14.5,
),
),
onTap: onEmojiSelected == null ? null : () => onEmojiSelected!(emoji),
);
},
);
}
}
```
Now it's time to use the `StreamEmojiAutocompleteOptions` widget.
```dart
StreamMessageInput(
customAutocompleteTriggers: [
StreamAutocompleteTrigger(
trigger: ':',
minimumRequiredCharacters: 2,
optionsViewBuilder: (
context,
autocompleteQuery,
messageEditingController,
) {
final query = autocompleteQuery.query;
return StreamEmojiAutocompleteOptions(
query: query,
onEmojiSelected: (emoji) {
// accepting the autocomplete option.
StreamAutocomplete.of(context).acceptAutocompleteOption(
emoji.char,
keepTrigger: false,
);
},
);
},
),
],
),
```
+2 -12
View File
@@ -1,9 +1,6 @@
name: stream_chat_flutter
repository: https://github.com/GetStream/stream-chat-flutter
versioning:
mode: independent
packages:
- packages/**
@@ -54,11 +51,11 @@ scripts:
description: Build all generated files for Dart & Flutter packages in this project.
generate:dart:
run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs"
run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs --enable-experiment=super-parameters,enhanced-enums"
description: Build all generated files for Dart packages in this project.
generate:flutter:
run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter pub run build_runner build --delete-conflicting-outputs"
run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter run build_runner build --delete-conflicting-outputs --enable-experiment=super-parameters,enhanced-enums"
description: Build all generated files for Flutter packages in this project.
test:all:
@@ -97,10 +94,3 @@ scripts:
npm install -g https://github.com/GetStream/stream-chat-docusaurus-cli &&
npx stream-chat-docusaurus -i -s
description: Runs the docusaurus documentation locally.
dev_dependencies:
dart_code_metrics: ^4.4.0
environment:
sdk: '>=2.17.0 <3.0.0'
flutter: '>=1.17.0 <3.0.0'
+73
View File
@@ -1,3 +1,76 @@
## 5.0.0
- Included the changes from version [4.5.0](#450).
🛑️ Breaking Changes from `5.0.0-beta.2`
- `Channel.addMembers`, `Channel.removeMembers`, `Channel.inviteMembers` and `Channel.update`
positional params are now optional params.
```dart
// previous
channel.addMembers([...ids], message, hideHistory);
channel.removeMembers([...ids], message);
channel.inviteMembers([...ids], message);
channel.update({...channelData}, updateMessage);
// new
channel.addMembers([...ids], message: ..., hideHistory: ...);
channel.removeMembers([...ids], message: ...);
channel.inviteMembers([...ids], message: ...);
channel.update({...channelData}, updateMessage: ...);
```
## 5.0.0-beta.2
- Included the changes from version [4.4.0](#440) and [4.4.1](#441).
## 5.0.0-beta.1
- Minor fixes.
- Removed deprecated code.
## 4.6.0
✅ Added
- Added `StreamChatClient.getCallToken` and `StreamChatClient.createCall` methods.
🐞 Fixed
- Only listen to client events when the user is connected to the websocket.
## 4.5.0
🐞 Fixed
- Fix `Channel.removeMessage` not able to remove thread message.
✅ Added
- Added `hide_history` flag in `client.addChannelMembers`, `channel.addMembers`.
## 4.4.1
🐞 Fixed
- Do not serialize `AttachmentFile.bytes`
## 4.4.0
🐞 Fixed
- Fix WebSocket contemporary connection calls while disconnecting
✅ Added
- Export `StreamAttachmentFileUploader`.
🔄 Changed
- Deprecated `StreamChatClient.attachmentFileUploader`,
Use `StreamChatClient.attachmentFileUploaderProvider` instead.
## 4.3.0
🐞 Fixed
+4 -1
View File
@@ -4,7 +4,10 @@ import 'package:stream_chat/stream_chat.dart';
Future<void> main() async {
/// Create a new instance of [StreamChatClient]
/// by passing the apikey obtained from your project dashboard.
final client = StreamChatClient('b67pax5b2wdq', logLevel: Level.INFO);
final client = StreamChatClient(
'b67pax5b2wdq',
logLevel: Level.INFO,
);
/// Set the current user. In a production scenario, this should be done using
/// a backend to generate a user token using our server SDK.
+148 -154
View File
@@ -3,12 +3,15 @@ import 'dart:math';
import 'package:collection/collection.dart'
show IterableExtension, ListEquality;
import 'package:dio/dio.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/client/retry_queue.dart';
import 'package:stream_chat/src/core/util/utils.dart';
import 'package:stream_chat/stream_chat.dart';
/// The maximum time the incoming [Event.typingStart] event is valid before a
/// [Event.typingStop] event is emitted automatically.
const incomingTypingStartEventTimeout = 7;
/// Class that manages a specific channel.
///
/// #### Channel name
@@ -1061,9 +1064,9 @@ class Channel {
/// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart
/// for more information.
Future<UpdateChannelResponse> update(
Map<String, Object?> channelData, [
Map<String, Object?> channelData, {
Message? updateMessage,
]) async {
}) async {
_checkInitialized();
return _client.updateChannel(
id!,
@@ -1146,27 +1149,34 @@ class Channel {
/// Add members to the channel.
Future<AddMembersResponse> addMembers(
List<String> memberIds, [
List<String> memberIds, {
Message? message,
]) async {
bool hideHistory = false,
}) async {
_checkInitialized();
return _client.addChannelMembers(id!, type, memberIds, message: message);
return _client.addChannelMembers(
id!,
type,
memberIds,
message: message,
hideHistory: hideHistory,
);
}
/// Invite members to the channel.
Future<InviteMembersResponse> inviteMembers(
List<String> memberIds, [
List<String> memberIds, {
Message? message,
]) async {
}) async {
_checkInitialized();
return _client.inviteChannelMembers(id!, type, memberIds, message: message);
}
/// Remove members from the channel.
Future<RemoveMembersResponse> removeMembers(
List<String> memberIds, [
List<String> memberIds, {
Message? message,
]) async {
}) async {
_checkInitialized();
return _client.removeChannelMembers(id!, type, memberIds, message: message);
}
@@ -1494,36 +1504,38 @@ class Channel {
)
.where((e) => e.cid == cid);
DateTime? _lastTypingEvent;
late final _keyStrokeHandler = KeyStrokeHandler(
onStartTyping: startTyping,
onStopTyping: stopTyping,
);
/// First of the [EventType.typingStart] and [EventType.typingStop] events
/// based on the users keystrokes. Call this on every keystroke.
/// Sends the [Event.typingStart] event and schedules a timer to invoke the
/// [Event.typingStop] event.
///
/// This is meant to be called every time the user presses a key.
Future<void> keyStroke([String? parentId]) async {
if (config?.typingEvents == false) {
return;
}
if (config?.typingEvents == false) return;
client.logger.info('start typing');
final now = DateTime.now();
if (_lastTypingEvent == null ||
now.difference(_lastTypingEvent!).inSeconds >= 2) {
_lastTypingEvent = now;
await sendEvent(Event(
type: EventType.typingStart,
parentId: parentId,
));
}
client.logger.info('KeyStroke received');
return _keyStrokeHandler(parentId);
}
/// Sets last typing to null and sends the typing.stop event.
/// Sends the [EventType.typingStart] event.
Future<void> startTyping([String? parentId]) async {
if (config?.typingEvents == false) return;
client.logger.info('start typing');
await sendEvent(Event(
type: EventType.typingStart,
parentId: parentId,
));
}
/// Sends the [EventType.typingStop] event.
Future<void> stopTyping([String? parentId]) async {
if (config?.typingEvents == false) {
return;
}
if (config?.typingEvents == false) return;
client.logger.info('stop typing');
_lastTypingEvent = null;
await sendEvent(Event(
type: EventType.typingStop,
parentId: parentId,
@@ -1533,6 +1545,7 @@ class Channel {
/// Call this method to dispose the channel client.
void dispose() {
state?.dispose();
_keyStrokeHandler.cancel();
}
void _checkInitialized() {
@@ -1594,9 +1607,9 @@ class ChannelClientState {
_listenMemberUnbanned();
_startCleaning();
_startCleaningStaleTypingEvents();
_startCleaningPinnedMessages();
_startCleaningStalePinnedMessages();
_channel._client.chatPersistenceClient
?.getChannelThreads(_channel.cid!)
@@ -1614,7 +1627,8 @@ class ChannelClientState {
});
}
final _subscriptions = <StreamSubscription>[];
final Channel _channel;
final _subscriptions = CompositeSubscription();
void _checkExpiredAttachmentMessages(ChannelState channelState) async {
final expiredAttachmentMessagesId = channelState.messages
@@ -1787,20 +1801,15 @@ class ChannelClientState {
/// Retry failed message.
Future<void> retryFailedMessages() async {
final failedMessages =
<Message>[...messages, ...threads.values.expand((v) => v)]
.where(
(message) =>
message.status != MessageSendingStatus.sent &&
message.createdAt.isBefore(
DateTime.now().subtract(
const Duration(
seconds: 5,
),
),
),
)
.toList();
final failedMessages = [...messages, ...threads.values.expand((v) => v)]
.where(
(message) =>
message.status != MessageSendingStatus.sent &&
message.createdAt.isBefore(
DateTime.now().subtract(const Duration(seconds: 5)),
),
)
.toList();
_retryQueue.add(failedMessages);
}
@@ -1956,11 +1965,13 @@ class ChannelClientState {
// Early return in case the thread is not available
if (!newThreads.containsKey(parentId)) return;
_threads = newThreads
..update(
parentId,
(messages) => messages..removeWhere((e) => e.id == message.id),
);
// Remove thread message shown in thread page.
newThreads.update(
parentId,
(messages) => [...messages.where((e) => e.id != message.id)],
);
_threads = newThreads;
// Early return if the thread message is not shown in channel.
if (message.showInChannel == false) return;
@@ -1985,12 +1996,7 @@ class ChannelClientState {
}
_subscriptions.add(
_channel
.on(
EventType.messageRead,
EventType.notificationMarkRead,
)
.listen(
_channel.on(EventType.messageRead, EventType.notificationMarkRead).listen(
(event) {
final readList = List<Read>.from(_channelState.read ?? []);
final userReadIndex =
@@ -2078,10 +2084,6 @@ class ChannelClientState {
(m) => m.user?.id == _channel.client.state.currentUser?.id,
);
/// User role for the current user.
@Deprecated('Please use currentUserChannelRole')
String? get currentUserRole => currentUserMember?.role;
/// Channel role for the current user
String? get currentUserChannelRole => currentUserMember?.channelRole;
@@ -2251,34 +2253,29 @@ class ChannelClientState {
);
}
/// Channel related typing users last value.
Map<User, Event> get typingEvents => _typingEventsController.value;
/// Channel related typing users stream.
Stream<Map<User, Event>> get typingEventsStream =>
_typingEventsController.stream;
final BehaviorSubject<Map<User, Event>> _typingEventsController =
BehaviorSubject.seeded({});
final Channel _channel;
final Map<User, Event> _typings = {};
/// Channel related typing users last value.
Map<User, Event> get typingEvents => _typingEventsController.value;
final _typingEventsController = BehaviorSubject.seeded(<User, Event>{});
void _listenTypingEvents() {
if (_channelState.channel?.config.typingEvents == false) {
return;
}
if (_channelState.channel?.config.typingEvents == false) return;
final currentUser = _channel.client.state.currentUser;
if (currentUser == null) return;
_subscriptions
..add(
_channel.on(EventType.typingStart).listen(
(event) {
if (event.user != null) {
final user = event.user!;
if (user.id != _channel.client.state.currentUser?.id) {
_typings[user] = event;
_typingEventsController.add(_typings);
}
final user = event.user;
if (user != null && user.id != currentUser.id) {
final events = {...typingEvents};
events[user] = event;
_typingEventsController.add(events);
}
},
),
@@ -2286,112 +2283,109 @@ class ChannelClientState {
..add(
_channel.on(EventType.typingStop).listen(
(event) {
if (event.user != null) {
final user = event.user!;
if (user.id != _channel.client.state.currentUser?.id) {
_typings.remove(event.user);
_typingEventsController.add(_typings);
}
final user = event.user;
if (user != null && user.id != currentUser.id) {
final events = {...typingEvents}..remove(user);
_typingEventsController.add(events);
}
},
),
)
..add(
_channel
.on()
.where((event) =>
event.user != null &&
members.any((m) => m.userId == event.user!.id))
.listen(
_channel.on().where((event) {
final user = event.user;
if (user == null) return false;
return members.any((m) => m.userId == user.id);
}).listen(
(event) {
final newMembers = List<Member>.from(members);
final oldMemberIndex =
newMembers.indexWhere((m) => m.userId == event.user!.id);
if (oldMemberIndex > -1) {
final oldMember = newMembers.removeAt(oldMemberIndex);
updateChannelState(ChannelState(
members: [
...newMembers,
oldMember.copyWith(
user: event.user,
),
],
));
updateChannelState(
ChannelState(
members: [
...newMembers,
oldMember.copyWith(
user: event.user,
),
],
),
);
}
},
),
);
}
Timer? _cleaningTimer;
Timer? _staleTypingEventsCleanerTimer;
void _startCleaning() {
if (_channelState.channel?.config.typingEvents == false) {
return;
}
// Checks and removes stale typing events that were not explicitly stopped by
// the sender due to technical difficulties. e.g. process death, loss of
// Internet connection or custom implementation.
void _startCleaningStaleTypingEvents() {
if (_channelState.channel?.config.typingEvents == false) return;
_cleaningTimer = Timer.periodic(const Duration(seconds: 1), (_) {
final now = DateTime.now();
if (_channel._lastTypingEvent != null &&
now.difference(_channel._lastTypingEvent!).inSeconds > 1) {
_channel.stopTyping();
}
_clean();
});
_staleTypingEventsCleanerTimer = Timer.periodic(
const Duration(seconds: 1),
(_) {
final now = DateTime.now();
typingEvents.forEach((user, event) {
if (now.difference(event.createdAt).inSeconds >
incomingTypingStartEventTimeout) {
_channel.client.handleEvent(
Event(
type: EventType.typingStop,
user: user,
cid: _channel.cid,
parentId: event.parentId,
),
);
}
});
},
);
}
late Timer _pinnedMessagesTimer;
Timer? _stalePinnedMessagesCleanerTimer;
void _startCleaningPinnedMessages() {
_pinnedMessagesTimer = Timer.periodic(const Duration(seconds: 30), (_) {
final now = DateTime.now();
var expiredMessages = channelState.pinnedMessages
?.where((m) => m.pinExpires?.isBefore(now) == true)
.toList();
if (expiredMessages != null && expiredMessages.isNotEmpty) {
expiredMessages = expiredMessages
.map((m) => m.copyWith(
pinExpires: null,
pinned: false,
))
// Checks and removes stale pinned messages that are not valid anymore.
void _startCleaningStalePinnedMessages() {
_stalePinnedMessagesCleanerTimer = Timer.periodic(
const Duration(seconds: 30),
(_) {
final now = DateTime.now();
var expiredMessages = channelState.pinnedMessages
?.where((m) => m.pinExpires?.isBefore(now) == true)
.toList();
if (expiredMessages != null && expiredMessages.isNotEmpty) {
expiredMessages = expiredMessages
.map((m) => m.copyWith(
pinExpires: null,
pinned: false,
))
.toList();
updateChannelState(_channelState.copyWith(
pinnedMessages: pinnedMessages.where(_pinIsValid).toList(),
messages: expiredMessages,
));
}
});
}
void _clean() {
final now = DateTime.now();
_typings.forEach((user, event) {
if (now.difference(event.createdAt).inSeconds > 7) {
_channel.client.handleEvent(
Event(
type: EventType.typingStop,
user: user,
cid: _channel.cid,
parentId: event.parentId,
),
);
}
});
updateChannelState(_channelState.copyWith(
pinnedMessages: pinnedMessages.where(_pinIsValid).toList(),
messages: expiredMessages,
));
}
},
);
}
/// Call this method to dispose this object.
void dispose() {
_debouncedUpdatePersistenceChannelState.cancel();
_retryQueue.dispose();
_subscriptions.forEach((s) => s.cancel());
_subscriptions.cancel();
_channelStateController.close();
_isUpToDateController.close();
_threadsController.close();
_cleaningTimer?.cancel();
_pinnedMessagesTimer.cancel();
_staleTypingEventsCleanerTimer?.cancel();
_stalePinnedMessagesCleanerTimer?.cancel();
_typingEventsController.close();
}
}
+111 -49
View File
@@ -70,7 +70,8 @@ class StreamChatClient {
Duration receiveTimeout = const Duration(seconds: 6),
StreamChatApi? chatApi,
WebSocket? ws,
AttachmentFileUploader? attachmentFileUploader,
AttachmentFileUploaderProvider attachmentFileUploaderProvider =
StreamAttachmentFileUploader.new,
}) {
logger.info('Initiating new StreamChatClient');
@@ -87,7 +88,7 @@ class StreamChatClient {
options: options,
tokenManager: _tokenManager,
connectionIdManager: _connectionIdManager,
attachmentFileUploader: attachmentFileUploader,
attachmentFileUploaderProvider: attachmentFileUploaderProvider,
logger: detachedLogger('🕸️'),
);
@@ -380,6 +381,10 @@ class StreamChatClient {
user,
includeUserDetails: includeUserDetailsInConnectCall,
);
// Start listening to events
state.subscribeToEvents();
return user.merge(event.me);
} catch (e, stk) {
logger.severe('error connecting ws', e, stk);
@@ -401,6 +406,9 @@ class StreamChatClient {
_connectionStatusSubscription?.cancel();
_connectionStatusSubscription = null;
// Stop listening to events
state.cancelEventSubscription();
_ws.disconnect();
}
@@ -568,6 +576,25 @@ class StreamChatClient {
}
}
/// Returns a token associated with the [callId].
Future<CallTokenPayload> getCallToken(String callId) async =>
_chatApi.call.getCallToken(callId);
/// Creates a new call.
Future<CreateCallPayload> createCall({
required String callId,
required String callType,
required String channelType,
required String channelId,
}) {
return _chatApi.call.createCall(
callId: callId,
callType: callType,
channelType: channelType,
channelId: channelId,
);
}
/// Requests channels with a given query from the API.
Future<List<Channel>> queryChannelsOnline({
Filter? filter,
@@ -1027,12 +1054,14 @@ class StreamChatClient {
String channelType,
List<String> memberIds, {
Message? message,
bool hideHistory = false,
}) =>
_chatApi.channel.addMembers(
channelId,
channelType,
memberIds,
message: message,
hideHistory: hideHistory,
);
/// Remove members from the channel
@@ -1452,29 +1481,40 @@ class StreamChatClient {
/// The class that handles the state of the channel listening to the events
class ClientState {
/// Creates a new instance listening to events and updating the state
ClientState(this._client) {
_subscriptions.addAll([
_client
ClientState(this._client);
CompositeSubscription? _eventsSubscription;
/// Starts listening to the client events.
void subscribeToEvents() {
if (_eventsSubscription != null) {
cancelEventSubscription();
}
_eventsSubscription = CompositeSubscription();
_eventsSubscription!
..add(_client
.on()
.where((event) =>
event.me != null && event.type != EventType.healthCheck)
.map((e) => e.me!)
.listen((user) => currentUser = currentUser?.merge(user) ?? user),
_client
.listen((user) {
currentUser = currentUser?.merge(user) ?? user;
}))
..add(_client
.on()
.map((event) => event.unreadChannels)
.whereType<int>()
.listen((count) {
currentUser = currentUser?.copyWith(unreadChannels: count);
}),
_client
}))
..add(_client
.on()
.map((event) => event.totalUnreadCount)
.whereType<int>()
.listen((count) {
currentUser = currentUser?.copyWith(totalUnreadCount: count);
}),
]);
}));
_listenChannelDeleted();
@@ -1485,56 +1525,73 @@ class ClientState {
_listenAllChannelsRead();
}
final _subscriptions = <StreamSubscription>[];
/// Stops listening to the client events.
void cancelEventSubscription() {
if (_eventsSubscription != null) {
_eventsSubscription!.cancel();
_eventsSubscription = null;
}
}
/// Used internally for optimistic update of unread count
set totalUnreadCount(int unreadCount) {
_totalUnreadCountController.add(unreadCount);
/// Pauses listening to the client events.
void pauseEventSubscription([Future<void>? resumeSignal]) {
_eventsSubscription?.pause(resumeSignal);
}
/// Resumes listening to the client events.
void resumeEventSubscription() {
_eventsSubscription?.resume();
}
void _listenChannelHidden() {
_subscriptions
.add(_client.on(EventType.channelHidden).listen((event) async {
final eventChannel = event.channel!;
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
channels[eventChannel.cid]?.dispose();
channels = channels..remove(eventChannel.cid);
}));
_eventsSubscription?.add(
_client.on(EventType.channelHidden).listen((event) async {
final eventChannel = event.channel!;
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
channels[eventChannel.cid]?.dispose();
channels = channels..remove(eventChannel.cid);
}),
);
}
void _listenUserUpdated() {
_subscriptions.add(_client.on(EventType.userUpdated).listen((event) {
if (event.user!.id == currentUser!.id) {
currentUser = OwnUser.fromJson(event.user!.toJson());
}
updateUser(event.user);
}));
_eventsSubscription?.add(
_client.on(EventType.userUpdated).listen((event) {
if (event.user!.id == currentUser!.id) {
currentUser = OwnUser.fromJson(event.user!.toJson());
}
updateUser(event.user);
}),
);
}
void _listenAllChannelsRead() {
_subscriptions
.add(_client.on(EventType.notificationMarkRead).listen((event) {
if (event.cid == null) {
channels.forEach((key, value) {
value.state?.unreadCount = 0;
});
}
}));
_eventsSubscription?.add(
_client.on(EventType.notificationMarkRead).listen((event) {
if (event.cid == null) {
channels.forEach((key, value) {
value.state?.unreadCount = 0;
});
}
}),
);
}
void _listenChannelDeleted() {
_subscriptions.add(_client
.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
EventType.notificationChannelDeleted,
)
.listen((Event event) async {
final eventChannel = event.channel!;
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
channels[eventChannel.cid]?.dispose();
channels = channels..remove(eventChannel.cid);
}));
_eventsSubscription?.add(
_client
.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
EventType.notificationChannelDeleted,
)
.listen((Event event) async {
final eventChannel = event.channel!;
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
channels[eventChannel.cid]?.dispose();
channels = channels..remove(eventChannel.cid);
}),
);
}
final StreamChatClient _client;
@@ -1596,6 +1653,11 @@ class ClientState {
_channelsController.add(newChannels);
}
/// Used internally for optimistic update of unread count
set totalUnreadCount(int unreadCount) {
_totalUnreadCountController.add(unreadCount);
}
void _computeUnreadCounts(OwnUser? user) {
final totalUnreadCount = user?.totalUnreadCount;
if (totalUnreadCount != null) {
@@ -1616,7 +1678,7 @@ class ClientState {
/// Call this method to dispose this object
void dispose() {
_subscriptions.forEach((s) => s.cancel());
cancelEventSubscription();
_currentUserController.close();
_unreadChannelsController.close();
_totalUnreadCountController.close();
@@ -0,0 +1,115 @@
import 'dart:async';
/// A class that manages buffering typing events and call [onTypingStarted] and
/// [onTypingStopped] accordingly in a timed manner.
///
/// This class is used by [Channel] to manage typing events.
class KeyStrokeHandler {
/// Creates a new instance of [KeyStrokeHandler].
KeyStrokeHandler({
this.startTypingEventTimeout = 1,
this.startTypingResendInterval = 3,
required this.onStartTyping,
required this.onStopTyping,
});
/// The number of seconds from the last [onStartTyping] callback until
/// the [onStopTyping] callback is automatically invoked.
final int startTypingEventTimeout;
/// The number of seconds after the last [onStartTyping] callback before
/// the [onStartTyping] callback is automatically invoked again.
final int startTypingResendInterval;
/// Called when a `typingStart` event needs to be send.
final Future<void> Function([String? parentId]) onStartTyping;
/// Called when a `typingStop` event needs to be send.
final Future<void> Function([String? parentId]) onStopTyping;
Timer? _keyStrokeTimer;
String? _currentParentId;
DateTime? _lastTypingEvent;
Completer<void>? _keyStrokeCompleter;
Future<void> _startTyping(String? parentId) {
_currentParentId = parentId;
_lastTypingEvent = DateTime.now();
return onStartTyping(parentId);
}
Future<void> _stopTyping(String? parentId) {
_currentParentId = null;
_lastTypingEvent = null;
return onStopTyping(parentId);
}
// Completes the key stroke completer if it is not yet completed.
void _completeKeyStrokeCompleterIfRequired() {
final completer = _keyStrokeCompleter;
if (completer != null && !completer.isCompleted) completer.complete();
}
// Completes the completer if available and not yet completed then creates a
// new completer and returns it.
Completer<void> _resetKeyStrokeCompleter() {
_completeKeyStrokeCompleterIfRequired();
return _keyStrokeCompleter = Completer<void>();
}
// Cancels the key stroke timer if it is running.
void _cancelKeyStrokeTimer() {
_keyStrokeTimer?.cancel();
_keyStrokeTimer = null;
}
/// Cancels the handler and stops the typing event.
void cancel() {
// If the user is typing, stop typing.
// This is needed to prevent the user from being stuck in typing mode.
if (_lastTypingEvent != null) {
// We don't need to handle the error here
// ignore: no-empty-block
_stopTyping(_currentParentId).catchError((_) {});
}
_cancelKeyStrokeTimer();
_completeKeyStrokeCompleterIfRequired();
}
/// Invokes the [onStartTyping] callback and schedules a timer to invoke the
/// [onStopTyping] callback.
///
/// This is meant to be called every time the user presses a key. The method
/// will manage requests and timer as needed.
Future<void> call([String? parentId]) async {
final completer = _resetKeyStrokeCompleter();
_cancelKeyStrokeTimer();
_keyStrokeTimer = Timer(Duration(seconds: startTypingEventTimeout), () {
_stopTyping(parentId).then((_) {
if (completer.isCompleted) return;
completer.complete();
}).onError((error, stackTrace) {
if (completer.isCompleted) return;
completer.completeError(error!, stackTrace);
});
});
// If the user is typing too long, it should call [onStartTyping] again.
final now = DateTime.now();
final lastTypingEvent = _lastTypingEvent;
if (lastTypingEvent == null ||
now.difference(lastTypingEvent).inMilliseconds >
// startTypingResendInterval in milliseconds
startTypingResendInterval * 1000) {
_startTyping(parentId).onError((error, stackTrace) {
_cancelKeyStrokeTimer();
if (completer.isCompleted) return;
completer.completeError(error!, stackTrace);
});
}
return completer.future;
}
}
@@ -3,6 +3,11 @@ import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/src/core/models/attachment_file.dart';
/// Signature for a function which provides instance of [AttachmentFileUploader]
typedef AttachmentFileUploaderProvider = AttachmentFileUploader Function(
StreamHttpClient httpClient,
);
/// Class responsible for uploading images and files to a given channel
abstract class AttachmentFileUploader {
/// Uploads a [image] to the given channel.
@@ -0,0 +1,41 @@
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
/// Defines the api dedicated to call operations.
class CallApi {
/// Initialize a new call api
CallApi(this._client);
final StreamHttpClient _client;
/// Returns a token dedicated to the [callId]
Future<CallTokenPayload> getCallToken(String callId) async {
final response = await _client.post(
'/calls/$callId',
data: {},
);
// return response.data;
return CallTokenPayload.fromJson(response.data);
}
/// Creates a new call
Future<CreateCallPayload> createCall({
required String callId,
required String callType,
required String channelType,
required String channelId,
}) async {
final response = await _client.post(
_getChannelUrl(channelId, channelType),
data: {
'id': callId,
'type': callType,
},
);
// return response.data;
return CreateCallPayload.fromJson(response.data);
}
String _getChannelUrl(String channelId, String channelType) =>
'/channels/$channelType/$channelId/call';
}
@@ -210,12 +210,14 @@ class ChannelApi {
String channelType,
List<String> memberIds, {
Message? message,
bool hideHistory = false,
}) async {
final response = await _client.post(
_getChannelUrl(channelId, channelType),
data: {
'add_members': memberIds,
'message': message,
'hide_history': hideHistory,
},
);
return AddMembersResponse.fromJson(response.data);
@@ -62,8 +62,6 @@ class PaginationParams extends Equatable {
/// ```
const PaginationParams({
this.limit = 10,
this.before = 10,
this.after = 10,
this.offset,
this.next,
this.idAround,
@@ -88,14 +86,6 @@ class PaginationParams extends Equatable {
/// The amount of items requested from the APIs.
final int limit;
/// The amount of items requested before message ID from the APIs.
@Deprecated('before is deprecated, use limit instead')
final int before;
/// The amount of items requested after message ID from the APIs.
@Deprecated('after is deprecated, use limit instead')
final int after;
/// The offset of requesting items.
final int? offset;
@@ -165,8 +155,6 @@ class PaginationParams extends Equatable {
}) =>
PaginationParams(
limit: limit ?? this.limit,
before: before ?? this.before,
after: limit ?? this.after,
offset: offset ?? this.offset,
idAround: idAround ?? this.idAround,
next: next ?? this.next,
@@ -186,8 +174,6 @@ class PaginationParams extends Equatable {
@override
List<Object?> get props => [
limit,
before,
after,
offset,
next,
idAround,
@@ -21,8 +21,6 @@ Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
PaginationParams(
limit: json['limit'] as int? ?? 10,
before: json['before'] as int? ?? 10,
after: json['after'] as int? ?? 10,
offset: json['offset'] as int?,
next: json['next'] as String?,
idAround: json['id_around'] as String?,
@@ -50,8 +48,6 @@ PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
final val = <String, dynamic>{
'limit': instance.limit,
'before': instance.before,
'after': instance.after,
};
void writeNotNull(String key, dynamic value) {
@@ -1,7 +1,9 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/client/client.dart';
import 'package:stream_chat/src/core/api/call_api.dart';
import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/core/models/banned_user.dart';
import 'package:stream_chat/src/core/models/call_payload.dart';
import 'package:stream_chat/src/core/models/channel_model.dart';
import 'package:stream_chat/src/core/models/channel_state.dart';
import 'package:stream_chat/src/core/models/device.dart';
@@ -495,3 +497,31 @@ class OGAttachmentResponse extends _BaseResponse {
static OGAttachmentResponse fromJson(Map<String, dynamic> json) =>
_$OGAttachmentResponseFromJson(json);
}
/// The response to [CallApi.getCallToken]
@JsonSerializable(createToJson: false)
class CallTokenPayload extends _BaseResponse {
/// Create a new instance from a [json].
static CallTokenPayload fromJson(Map<String, dynamic> json) =>
_$CallTokenPayloadFromJson(json);
/// The token to use for the call.
String? token;
/// The user id specific to Agora.
int? agoraUid;
/// The appId specific to Agora.
String? agoraAppId;
}
/// The response to [CallApi.createCall]
@JsonSerializable(createToJson: false)
class CreateCallPayload extends _BaseResponse {
/// Create a new instance from a [json].
static CreateCallPayload fromJson(Map<String, dynamic> json) =>
_$CreateCallPayloadFromJson(json);
/// The call object.
CallPayload? call;
}
@@ -297,3 +297,17 @@ OGAttachmentResponse _$OGAttachmentResponseFromJson(
..title = json['title'] as String?
..titleLink = json['title_link'] as String?
..type = json['type'] as String?;
CallTokenPayload _$CallTokenPayloadFromJson(Map<String, dynamic> json) =>
CallTokenPayload()
..duration = json['duration'] as String?
..token = json['token'] as String?
..agoraUid = json['agora_uid'] as int?
..agoraAppId = json['agora_app_id'] as String?;
CreateCallPayload _$CreateCallPayloadFromJson(Map<String, dynamic> json) =>
CreateCallPayload()
..duration = json['duration'] as String?
..call = json['call'] == null
? null
: CallPayload.fromJson(json['call'] as Map<String, dynamic>);
@@ -1,5 +1,6 @@
import 'package:logging/logging.dart';
import 'package:stream_chat/src/core/api/attachment_file_uploader.dart';
import 'package:stream_chat/src/core/api/call_api.dart';
import 'package:stream_chat/src/core/api/channel_api.dart';
import 'package:stream_chat/src/core/api/device_api.dart';
import 'package:stream_chat/src/core/api/general_api.dart';
@@ -22,9 +23,10 @@ class StreamChatApi {
StreamHttpClientOptions? options,
TokenManager? tokenManager,
ConnectionIdManager? connectionIdManager,
AttachmentFileUploader? attachmentFileUploader,
AttachmentFileUploaderProvider attachmentFileUploaderProvider =
StreamAttachmentFileUploader.new,
Logger? logger,
}) : _fileUploader = attachmentFileUploader,
}) : _fileUploaderProvider = attachmentFileUploaderProvider,
_client = client ??
StreamHttpClient(
apiKey,
@@ -35,6 +37,7 @@ class StreamChatApi {
);
final StreamHttpClient _client;
final AttachmentFileUploaderProvider _fileUploaderProvider;
UserApi? _user;
@@ -51,6 +54,11 @@ class StreamChatApi {
/// Api dedicated to message operations
MessageApi get message => _message ??= MessageApi(_client);
CallApi? _call;
/// Api dedicated to call operations
CallApi get call => _call ??= CallApi(_client);
ChannelApi? _channel;
/// Api dedicated to channel operations
@@ -75,5 +83,5 @@ class StreamChatApi {
/// Class responsible for uploading images and files to a given channel
AttachmentFileUploader get fileUploader =>
_fileUploader ??= StreamAttachmentFileUploader(_client);
_fileUploader ??= _fileUploaderProvider.call(_client);
}
@@ -49,7 +49,7 @@ class Attachment extends Equatable {
if (file?.mimeType != null) 'mime_type': file?.mimeType?.mimeType,
} {
this.uploadState = uploadState ??
((assetUrl != null || imageUrl != null)
((assetUrl != null || imageUrl != null || thumbUrl != null)
? const UploadState.success()
: const UploadState.preparing());
}
@@ -47,11 +47,12 @@ class AttachmentFile {
final String? _name;
/// File name including its extension.
String? get name => _name ?? path?.split('/').last;
String? get name =>
_name ?? path?.split(CurrentPlatform.isWindows ? r'\' : '/').last;
/// Byte data for this file. Particularly useful if you want to manipulate
/// its data or easily upload to somewhere else.
@JsonKey(toJson: _toString, fromJson: _fromString)
@JsonKey(ignore: true)
final Uint8List? bytes;
/// The file size in bytes.
@@ -85,6 +86,22 @@ class AttachmentFile {
}
return multiPartFile;
}
/// Creates a copy of this [AttachmentFile] but with the given fields
/// replaced with the new values.
AttachmentFile copyWith({
String? path,
String? name,
Uint8List? bytes,
int? size,
}) {
return AttachmentFile(
path: path ?? this.path,
name: name ?? this.name,
bytes: bytes ?? this.bytes,
size: size ?? this.size,
);
}
}
/// Union class to hold various [UploadState] of a attachment.
@@ -124,13 +141,3 @@ class UploadState with _$UploadState {
/// Returns true if state is [Failed]
bool get isFailed => this is Failed;
}
Uint8List? _fromString(String? bytes) {
if (bytes == null) return null;
return Uint8List.fromList(bytes.codeUnits);
}
String? _toString(Uint8List? bytes) {
if (bytes == null) return null;
return String.fromCharCodes(bytes);
}
@@ -224,7 +224,9 @@ class _$Preparing extends Preparing {
@override
Map<String, dynamic> toJson() {
return _$$PreparingToJson(this);
return _$$PreparingToJson(
this,
);
}
}
@@ -392,7 +394,9 @@ class _$InProgress extends InProgress {
@override
Map<String, dynamic> toJson() {
return _$$InProgressToJson(this);
return _$$InProgressToJson(
this,
);
}
}
@@ -404,8 +408,8 @@ abstract class InProgress extends UploadState {
factory InProgress.fromJson(Map<String, dynamic> json) =
_$InProgress.fromJson;
int get uploaded => throw _privateConstructorUsedError;
int get total => throw _privateConstructorUsedError;
int get uploaded;
int get total;
@JsonKey(ignore: true)
_$$InProgressCopyWith<_$InProgress> get copyWith =>
throw _privateConstructorUsedError;
@@ -531,7 +535,9 @@ class _$Success extends Success {
@override
Map<String, dynamic> toJson() {
return _$$SuccessToJson(this);
return _$$SuccessToJson(
this,
);
}
}
@@ -686,7 +692,9 @@ class _$Failed extends Failed {
@override
Map<String, dynamic> toJson() {
return _$$FailedToJson(this);
return _$$FailedToJson(
this,
);
}
}
@@ -696,7 +704,7 @@ abstract class Failed extends UploadState {
factory Failed.fromJson(Map<String, dynamic> json) = _$Failed.fromJson;
String get error => throw _privateConstructorUsedError;
String get error;
@JsonKey(ignore: true)
_$$FailedCopyWith<_$Failed> get copyWith =>
throw _privateConstructorUsedError;
@@ -11,14 +11,12 @@ AttachmentFile _$AttachmentFileFromJson(Map<String, dynamic> json) =>
size: json['size'] as int?,
path: json['path'] as String?,
name: json['name'] as String?,
bytes: _fromString(json['bytes'] as String?),
);
Map<String, dynamic> _$AttachmentFileToJson(AttachmentFile instance) =>
<String, dynamic>{
'path': instance.path,
'name': instance.name,
'bytes': _toString(instance.bytes),
'size': instance.size,
};
@@ -0,0 +1,72 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'call_payload.g.dart';
/// Model containing the information about a call.
@JsonSerializable(createToJson: false)
class CallPayload extends Equatable {
/// Create a new instance.
const CallPayload({
required this.id,
required this.provider,
this.agora,
this.hms,
});
/// Create a new instance from a [json].
factory CallPayload.fromJson(Map<String, dynamic> json) =>
_$CallPayloadFromJson(json);
/// The call id.
final String id;
/// The call provider.
final String provider;
/// The payload specific to Agora.
final AgoraPayload? agora;
/// The payload specific to 100ms.
final HMSPayload? hms;
@override
List<Object?> get props => [id, provider, agora, hms];
}
/// Payload for Agora call.
@JsonSerializable(createToJson: false)
class AgoraPayload extends Equatable {
/// Create a new instance.
const AgoraPayload({required this.channel});
/// Create a new instance from a [json].
factory AgoraPayload.fromJson(Map<String, dynamic> json) =>
_$AgoraPayloadFromJson(json);
/// The Agora channel.
final String channel;
@override
List<Object?> get props => [channel];
}
/// Payload for 100ms call.
@JsonSerializable(createToJson: false)
class HMSPayload extends Equatable {
/// Create a new instance.
const HMSPayload({required this.roomId, required this.roomName});
/// Create a new instance from a [json].
factory HMSPayload.fromJson(Map<String, dynamic> json) =>
_$HMSPayloadFromJson(json);
/// The id of the 100ms room.
final String roomId;
/// The name of the 100ms room.
final String roomName;
@override
List<Object?> get props => [roomId, roomName];
}
@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'call_payload.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CallPayload _$CallPayloadFromJson(Map<String, dynamic> json) => CallPayload(
id: json['id'] as String,
provider: json['provider'] as String,
agora: json['agora'] == null
? null
: AgoraPayload.fromJson(json['agora'] as Map<String, dynamic>),
hms: json['hms'] == null
? null
: HMSPayload.fromJson(json['hms'] as Map<String, dynamic>),
);
AgoraPayload _$AgoraPayloadFromJson(Map<String, dynamic> json) => AgoraPayload(
channel: json['channel'] as String,
);
HMSPayload _$HMSPayloadFromJson(Map<String, dynamic> json) => HMSPayload(
roomId: json['room_id'] as String,
roomName: json['room_name'] as String,
);
@@ -39,7 +39,6 @@ class ChannelModel {
createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now(),
// TODO: Make them top-level fields in v5
// For backwards compatibility, set 'disabled', 'hidden'
// and 'truncated_at' in [extraData].
extraData = {
@@ -16,7 +16,6 @@ class Member extends Equatable {
this.inviteAcceptedAt,
this.inviteRejectedAt,
this.invited = false,
this.role,
this.channelRole,
this.userId,
this.isModerator = false,
@@ -48,10 +47,6 @@ class Member extends Equatable {
/// True if the user has been invited to the channel
final bool invited;
/// The role of the user in the channel
@Deprecated('Please use channelRole')
final String? role;
/// The role of this member in the channel
final String? channelRole;
@@ -100,7 +95,6 @@ class Member extends Equatable {
banned: banned ?? this.banned,
banExpires: banExpires ?? this.banExpires,
shadowBanned: shadowBanned ?? this.shadowBanned,
role: role ?? this.role,
channelRole: channelRole ?? this.channelRole,
userId: userId ?? this.userId,
isModerator: isModerator ?? this.isModerator,
@@ -117,7 +111,6 @@ class Member extends Equatable {
inviteAcceptedAt,
inviteRejectedAt,
invited,
role,
channelRole,
userId,
isModerator,
@@ -17,7 +17,6 @@ Member _$MemberFromJson(Map<String, dynamic> json) => Member(
? null
: DateTime.parse(json['invite_rejected_at'] as String),
invited: json['invited'] as bool? ?? false,
role: json['role'] as String?,
channelRole: json['channel_role'] as String?,
userId: json['user_id'] as String?,
isModerator: json['is_moderator'] as bool? ?? false,
@@ -39,7 +38,6 @@ Map<String, dynamic> _$MemberToJson(Member instance) => <String, dynamic>{
'invite_accepted_at': instance.inviteAcceptedAt?.toIso8601String(),
'invite_rejected_at': instance.inviteRejectedAt?.toIso8601String(),
'invited': instance.invited,
'role': instance.role,
'channel_role': instance.channelRole,
'user_id': instance.userId,
'is_moderator': instance.isModerator,
@@ -46,7 +46,6 @@ class User extends Equatable {
this.language,
}) : createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now(),
// TODO: Make them top-level fields in v5
// For backwards compatibility, set 'name', 'image' in [extraData].
extraData = {
...extraData,
@@ -4,25 +4,25 @@ import 'package:stream_chat/src/core/platform_detector/platform_detector_stub.da
/// Possible platforms
enum PlatformType {
///
/// Android: <https://www.android.com/>
android,
///
/// iOS: <https://www.apple.com/ios/>
ios,
///
/// web: <https://en.wikipedia.org/wiki/World_Wide_Web>
web,
///
/// macOS: <https://www.apple.com/macos>
macOS,
///
/// Windows: <https://www.windows.com>
windows,
///
/// Linux: <https://www.linux.org>
linux,
///
/// Fuchsia: <https://fuchsia.dev/fuchsia-src/concepts>
fuchsia,
}
@@ -51,6 +51,9 @@ class CurrentPlatform {
/// True if the app is running on fuchsia
static bool get isFuchsia => type == PlatformType.fuchsia;
/// True if the app is running in test environment
static bool get isFlutterTest => isFlutterTestEnvironment;
/// Returns a string version of the platform
static String get name {
switch (type) {
@@ -68,8 +71,6 @@ class CurrentPlatform {
return 'linux';
case PlatformType.fuchsia:
return 'fuchsia';
default:
return '';
}
}
@@ -10,3 +10,8 @@ PlatformType get currentPlatform {
if (Platform.isIOS) return PlatformType.ios;
return PlatformType.android;
}
/// True if the app is running in test environment.
bool get isFlutterTestEnvironment {
return Platform.environment.containsKey('FLUTTER_TEST');
}
@@ -1,6 +1,7 @@
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
/// Stub implementation
PlatformType get currentPlatform {
throw UnimplementedError();
}
PlatformType get currentPlatform => throw UnimplementedError();
/// Stub implementation
bool get isFlutterTestEnvironment => throw UnimplementedError();
@@ -2,3 +2,8 @@ import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
/// Version running on web
PlatformType get currentPlatform => PlatformType.web;
/// True if the app is running in test environment.
///
/// Always returns false as we don't have environment variables on web.
bool get isFlutterTestEnvironment => false;
@@ -1,4 +1,5 @@
import 'package:stream_chat/src/core/api/requests.dart';
import 'package:stream_chat/src/core/models/attachment_file.dart';
import 'package:stream_chat/src/core/models/channel_model.dart';
import 'package:stream_chat/src/core/models/channel_state.dart';
import 'package:stream_chat/src/core/models/event.dart';
@@ -8,6 +9,7 @@ import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/core/models/reaction.dart';
import 'package:stream_chat/src/core/models/read.dart';
import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
import 'package:stream_chat/src/core/util/extension.dart';
/// A simple client used for persisting chat data locally.
@@ -133,7 +135,7 @@ abstract class ChatPersistenceClient {
/// Remove a pinned message by message [cids]
Future<void> deletePinnedMessageByCids(List<String> cids);
/// Remove a channel by [cid]
/// Remove a channel by [channelId]
Future<void> deleteChannels(List<String> cids);
/// Updates the message data of a particular channel [cid] with
@@ -243,7 +245,15 @@ abstract class ChatPersistenceClient {
final cid = channel.cid;
final reads = state.read;
final members = state.members;
final messages = state.messages;
final Iterable<Message>? messages;
if (CurrentPlatform.isWeb) {
messages = state.messages?.where((it) => !it.attachments.any(
(attachment) =>
attachment.uploadState != const UploadState.success(),
));
} else {
messages = state.messages;
}
final pinnedMessages = state.pinnedMessages;
// Preparing deletion data
@@ -255,7 +265,7 @@ abstract class ChatPersistenceClient {
// preparing addition data
channelWithReads[cid] = reads;
channelWithMembers[cid] = members;
channelWithMessages[cid] = messages;
channelWithMessages[cid] = messages?.toList();
channelWithPinnedMessages[cid] = pinnedMessages;
reactions.addAll(messages?.expand(_expandReactions) ?? []);
@@ -436,6 +436,9 @@ class WebSocket with TimerHelper {
/// Disconnects the WS and releases eventual resources
void disconnect() {
if (connectionStatus == ConnectionStatus.disconnected) return;
_resetRequestFlags(resetAttempts: true);
_connectionStatus = ConnectionStatus.disconnected;
_logger?.info('Disconnecting web-socket connection');
@@ -447,6 +450,7 @@ class WebSocket with TimerHelper {
_stopMonitoringEvents();
_manuallyClosed = true;
_closeWebSocketChannel();
}
}
+5 -3
View File
@@ -1,6 +1,7 @@
library stream_chat;
export 'package:async/async.dart';
export 'package:dio/src/cancel_token.dart';
export 'package:dio/src/dio_error.dart';
export 'package:dio/src/multipart_file.dart';
export 'package:dio/src/options.dart';
@@ -9,8 +10,7 @@ export 'package:logging/logging.dart' show Logger, Level, LogRecord;
export 'package:rate_limiter/rate_limiter.dart';
export 'package:uuid/uuid.dart';
export './src/core/api/attachment_file_uploader.dart'
show AttachmentFileUploader;
export './src/core/api/attachment_file_uploader.dart';
export './src/core/api/requests.dart';
export './src/core/api/requests.dart';
export './src/core/api/responses.dart';
@@ -40,11 +40,12 @@ export './src/permission_type.dart';
export './src/ws/connection_status.dart';
export 'src/client/channel.dart';
export 'src/client/client.dart';
export 'src/client/key_stroke_handler.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/api/stream_chat_api.dart';
export 'src/core/error/error.dart';
export 'src/core/models/action.dart';
export 'src/core/models/attachment.dart';
@@ -63,6 +64,7 @@ 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/platform_detector/platform_detector.dart';
export 'src/core/util/extension.dart';
export 'src/db/chat_persistence_client.dart';
export 'src/event_type.dart';
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names
const PACKAGE_VERSION = '4.3.0';
const PACKAGE_VERSION = '5.0.0';
+1 -1
View File
@@ -1,7 +1,7 @@
name: stream_chat
homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications.
version: 4.3.0
version: 5.0.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -0,0 +1,5 @@
{
"size": 12,
"path": "/me/user/test.jpg",
"name": "test.jpg"
}
@@ -108,7 +108,6 @@ void main() {
});
});
// TODO : test all persistence related logic in this group
group('Initialized Channel with Persistence', () {
late final client = MockStreamChatClientWithPersistence();
const channelId = 'test-channel-id';
@@ -1832,7 +1831,10 @@ void main() {
..message = updateMessage,
);
final res = await channel.update(channelData, updateMessage);
final res = await channel.update(
channelData,
updateMessage: updateMessage,
);
expect(res, isNotNull);
expect(res.channel.cid, channelModel.cid);
@@ -2028,7 +2030,7 @@ void main() {
..message = message,
);
final res = await channel.addMembers(memberIds, message);
final res = await channel.addMembers(memberIds, message: message);
expect(res, isNotNull);
expect(res.channel.cid, channelModel.cid);
@@ -2060,7 +2062,7 @@ void main() {
..message = message,
);
final res = await channel.inviteMembers(memberIds, message);
final res = await channel.inviteMembers(memberIds, message: message);
expect(res, isNotNull);
expect(res.channel.cid, channelModel.cid);
@@ -2093,7 +2095,7 @@ void main() {
..message = message,
);
final res = await channel.removeMembers(memberIds, message);
final res = await channel.removeMembers(memberIds, message: message);
expect(res, isNotNull);
expect(res.channel.cid, channelModel.cid);
@@ -2658,16 +2660,22 @@ void main() {
});
test(
'''should send `typingStart` event if there is not already a typingEvent or the difference between the two is >= 2 seconds''',
'''should send `typingStart` event if there is not already a typingEvent or the difference between the two is > 3 seconds''',
() async {
final typingEvent = Event(type: EventType.typingStart);
final startTypingEvent = Event(type: EventType.typingStart);
final stopTypingEvent = Event(type: EventType.typingStop);
when(() => channel.config?.typingEvents).thenReturn(true);
when(() => client.sendEvent(
channelId,
channelType,
any(that: isSameEventAs(typingEvent)),
any(that: isSameEventAs(startTypingEvent)),
)).thenAnswer((_) async => EmptyResponse());
when(() => client.sendEvent(
channelId,
channelType,
any(that: isSameEventAs(stopTypingEvent)),
)).thenAnswer((_) async => EmptyResponse());
await channel.keyStroke();
@@ -2675,7 +2683,12 @@ void main() {
verify(() => client.sendEvent(
channelId,
channelType,
any(that: isSameEventAs(typingEvent)),
any(that: isSameEventAs(startTypingEvent)),
)).called(1);
verify(() => client.sendEvent(
channelId,
channelType,
any(that: isSameEventAs(stopTypingEvent)),
)).called(1);
},
);
@@ -2688,7 +2701,7 @@ void main() {
final typingStopEvent = Event(type: EventType.typingStop);
await channel.keyStroke();
await channel.stopTyping();
verifyNever(() => client.sendEvent(
channelId,
@@ -0,0 +1,73 @@
// ignore_for_file: avoid_redundant_argument_values
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
mixin OnKeyStrokeEvent {
Future<void> call([String? parentId]);
}
class OnStartTyping extends Mock implements OnKeyStrokeEvent {}
class OnStopTyping extends Mock implements OnKeyStrokeEvent {}
void main() {
final onStartTyping = OnStartTyping();
final onStopTyping = OnStopTyping();
late KeyStrokeHandler keyStrokeHandler;
const startTypingEventTimeout = 1;
const startTypingResendInterval = 2;
setUp(() {
when(() => onStartTyping(any())).thenAnswer((_) => Future.value());
when(() => onStopTyping(any())).thenAnswer((_) => Future.value());
keyStrokeHandler = KeyStrokeHandler(
onStartTyping: onStartTyping,
onStopTyping: onStopTyping,
startTypingEventTimeout: startTypingEventTimeout,
startTypingResendInterval: startTypingResendInterval,
);
});
tearDown(() {
keyStrokeHandler.cancel();
clearInteractions(onStartTyping);
clearInteractions(onStopTyping);
});
group('call', () {
test('should work fine', () {
expect(keyStrokeHandler.call(), completes);
});
test('should call onStartTyping', () async {
keyStrokeHandler.call();
verify(() => onStartTyping(any())).called(1);
});
test('should call onStopTyping', () async {
keyStrokeHandler
..call()
..cancel();
verify(() => onStopTyping(any())).called(1);
});
test('should call onStartTyping after startTypingResendInterval', () async {
final watch = Stopwatch()..start();
while (watch.elapsed.inSeconds <= startTypingResendInterval) {
keyStrokeHandler.call();
}
watch.stop();
verify(() => onStartTyping(any())).called(2);
});
test('should call onStopTyping after startTypingEventTimeout', () async {
keyStrokeHandler.call();
await Future.delayed(const Duration(seconds: startTypingEventTimeout));
verify(() => onStopTyping(any())).called(1);
});
});
}
@@ -68,6 +68,4 @@ void main() {
expect(retryQueue.hasMessages, isTrue);
});
});
// TODO: Add more tests once macbook is fixed :(
}
@@ -0,0 +1,66 @@
import 'package:dio/dio.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/src/core/api/call_api.dart';
import 'package:test/test.dart';
import '../../mocks.dart';
void main() {
Response successResponse(String path, {Object? data}) => Response(
data: data,
requestOptions: RequestOptions(path: path),
statusCode: 200,
);
late final client = MockHttpClient();
late CallApi callApi;
setUp(() {
callApi = CallApi(client);
});
test('getCallToken should work', () async {
const callId = 'test-call-id';
const path = '/calls/$callId';
when(() => client.post(path, data: {})).thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{}));
final res = await callApi.getCallToken(callId);
expect(res, isNotNull);
verify(() => client.post(path, data: any(named: 'data'))).called(1);
verifyNoMoreInteractions(client);
});
test('createCall should work', () async {
const callId = 'test-call-id';
const callType = 'test-call-type';
const channelType = 'test-channel-type';
const channelId = 'test-channel-id';
const path = '/channels/$channelType/$channelId/call';
when(() => client.post(
path,
data: {
'id': callId,
'type': callType,
},
))
.thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{}));
final res = await callApi.createCall(
callId: callId,
callType: callType,
channelType: channelType,
channelId: channelId,
);
expect(res, isNotNull);
verify(() => client.post(path, data: any(named: 'data'))).called(1);
verifyNoMoreInteractions(client);
});
}
@@ -376,6 +376,7 @@ void main() {
const memberIds = ['test-member-id-1', 'test-member-id-2'];
final channelModel = ChannelModel(id: channelId, type: channelType);
final message = Message(id: 'test-message-id', text: 'members-added');
const hideHistory = true;
final path = _getChannelUrl(channelId, channelType);
@@ -384,6 +385,7 @@ void main() {
data: {
'add_members': memberIds,
'message': message,
'hide_history': hideHistory,
},
)).thenAnswer((_) async => successResponse(path, data: {
'channel': channelModel.toJson(),
@@ -395,6 +397,7 @@ void main() {
channelType,
memberIds,
message: message,
hideHistory: hideHistory,
);
expect(res, isNotNull);
@@ -26,6 +26,56 @@ void main() {
}
},
);
test('copyWith', () {
final params = PaginationParams(
offset: 10,
limit: 20,
createdAtAfter: DateTime.now(),
createdAtAfterOrEqual: DateTime.now(),
createdAtAround: DateTime.now(),
createdAtBefore: DateTime.now(),
createdAtBeforeOrEqual: DateTime.now(),
greaterThan: 'greater-than',
greaterThanOrEqual: 'greater-than-or-equal',
lessThan: 'less-than',
lessThanOrEqual: 'less-than-or-equal',
idAround: 'id-around',
);
final sameOld = params.copyWith();
expect(sameOld, equals(params));
final newDateTime = DateTime.now().add(const Duration(days: 2));
const newTestString = 'test';
final newParams = params.copyWith(
limit: 2,
offset: 2,
createdAtAfter: newDateTime,
createdAtAfterOrEqual: newDateTime,
createdAtAround: newDateTime,
createdAtBefore: newDateTime,
createdAtBeforeOrEqual: newDateTime,
greaterThan: newTestString,
greaterThanOrEqual: newTestString,
lessThan: newTestString,
lessThanOrEqual: newTestString,
idAround: newTestString,
);
expect(newParams.limit, 2);
expect(newParams.offset, 2);
expect(newParams.createdAtAfter, newDateTime);
expect(newParams.createdAtAfterOrEqual, newDateTime);
expect(newParams.createdAtAround, newDateTime);
expect(newParams.createdAtBefore, newDateTime);
expect(newParams.createdAtBeforeOrEqual, newDateTime);
expect(newParams.greaterThan, newTestString);
expect(newParams.greaterThanOrEqual, newTestString);
expect(newParams.lessThan, newTestString);
expect(newParams.lessThanOrEqual, newTestString);
expect(newParams.idAround, newTestString);
});
});
});
}
@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:stream_chat/src/core/models/call_payload.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
@@ -4340,5 +4341,31 @@ void main() {
expect(response.members, isA<List<Member>>());
expect(response.message, isA<Message>());
});
test('CallTokenPayload', () {
const jsonExample = '''
{"duration": "3ms",
"agora_app_id":"test",
"agora_uid": 12,
"token": "token"}
''';
final response = CallTokenPayload.fromJson(json.decode(jsonExample));
expect(response.agoraAppId, isA<String>());
expect(response.agoraUid, isA<int>());
expect(response.token, isA<String>());
});
test('CreateCallPayload', () {
const jsonExample = '''
{"call":
{"id":"test",
"provider": "test",
"agora": {"channel":"test"},
"hms":{"room_id":"test", "room_name":"test"}
}}
''';
final response = CreateCallPayload.fromJson(json.decode(jsonExample));
expect(response.call, isA<CallPayload>());
});
});
}
@@ -0,0 +1,39 @@
import 'dart:typed_data';
import 'package:stream_chat/src/core/models/attachment_file.dart';
import 'package:test/test.dart';
import '../../utils.dart';
void main() {
group('src/models/attachment_file', () {
test('should parse json correctly', () {
final attachment =
AttachmentFile.fromJson(jsonFixture('attachment_file.json'));
expect(attachment.name, 'test.jpg');
expect(attachment.size, 12);
expect(
attachment.path,
'/me/user/test.jpg',
);
});
test('should serialize to json correctly', () {
final attachment = AttachmentFile(
size: 12,
bytes: Uint8List.fromList([1, 2, 3]),
name: 'test.jpg',
path: '/me/user/test.jpg',
);
expect(
attachment.toJson(),
{
'size': 12,
'name': 'test.jpg',
'path': '/me/user/test.jpg',
},
);
});
});
}
@@ -0,0 +1,38 @@
import 'dart:convert';
import 'package:stream_chat/src/core/models/call_payload.dart';
import 'package:test/test.dart';
void main() {
test('CallPayload', () {
const jsonExample = '''
{"id":"test",
"provider": "test",
"agora": {"channel":"test"},
"hms":{"room_id":"test", "room_name":"test"}
}
''';
final response = CallPayload.fromJson(json.decode(jsonExample));
expect(response.agora, isA<AgoraPayload>());
expect(response.hms, isA<HMSPayload>());
expect(response.id, isA<String>());
expect(response.provider, isA<String>());
});
test('AgoraPayload', () {
const jsonExample = '''
{"channel":"test"}
''';
final response = AgoraPayload.fromJson(json.decode(jsonExample));
expect(response.channel, isA<String>());
});
test('HMSPayload', () {
const jsonExample = '''
{"room_id":"test", "room_name":"test"}
''';
final response = HMSPayload.fromJson(json.decode(jsonExample));
expect(response.roomId, isA<String>());
expect(response.roomName, isA<String>());
});
}
-1
View File
@@ -8,7 +8,6 @@ import 'package:stream_chat/src/core/api/general_api.dart';
import 'package:stream_chat/src/core/api/guest_api.dart';
import 'package:stream_chat/src/core/api/message_api.dart';
import 'package:stream_chat/src/core/api/moderation_api.dart';
import 'package:stream_chat/src/core/api/stream_chat_api.dart';
import 'package:stream_chat/src/core/api/user_api.dart';
import 'package:stream_chat/src/core/http/token.dart';
import 'package:stream_chat/src/core/http/token_manager.dart';
@@ -131,6 +131,40 @@ void main() {
addTearDown(timer.cancel);
});
test('`connect`, `disconnect` and `connect` again without waiting', () async {
final user = OwnUser(
id: 'test-user',
name: 'test',
);
const connectionId = 'test-connection-id';
// Sends connect event to web-socket stream
final timer = Timer.periodic(const Duration(milliseconds: 300), (_) {
final event = Event(
type: EventType.healthCheck,
connectionId: connectionId,
me: user,
);
webSocketSink.add(json.encode(event));
});
await webSocket.connect(
user,
);
webSocket
..disconnect()
..connect(user)
..disconnect();
final event = await webSocket.connect(user);
expect(event.type, EventType.healthCheck);
expect(event.connectionId, connectionId);
expect(event.me, isNotNull);
expect(event.me!.id, user.id);
addTearDown(timer.cancel);
});
test('`connect` should throw if already in connection attempt', () async {
final user = OwnUser(id: 'test-user');
webSocket.connect(user);
+189 -1
View File
@@ -1,3 +1,190 @@
## 5.0.0
- Included the changes from version [4.5.0](#450).
🐞 Fixed
- [[#1326]](https://github.com/GetStream/stream-chat-flutter/issues/1326) Fixed hitting "enter" on
the android keyboard sends the message instead of going to a new line.
✅ Added
- Added `StreamMemberGridView` and `StreamMemberListView`.
- Added support for additional text field params in `StreamMessageInput`
* `maxLines`
* `minLines`
* `textInputAction`
* `keyboardType`
* `textCapitalization`
- Added `showStreamAttachmentPickerModalBottomSheet` to show the attachment picker modal bottom sheet.
🔄 Changed
- Removed Emoji picker from `StreamMessageInput`.
## 5.0.0-beta.2
- Included the changes from version [4.4.0](#440) and [4.4.1](#441).
🐞 Fixed
- Fixed the unread message header in the message list view.
- Show dialog after clicking on the camera button and permission is denied.
- Fix Jiffy initialization.
- Fix loading to unread position in `StreamMessageListView`.
- Minor fixes and improvements.
🔄 Changed
- [[#1125]](https://github.com/GetStream/stream-chat-flutter/issues/1125) `defaultUserImage`
, `placeholderUserImage`, `reactionIcons`, and `enforceUniqueReactions` have been refactored out
of `StreamChatThemeData` and into the new `StreamChatConfigurationData` class.
✅ Added
- Added `StreamAutocomplete` widget for autocomplete triggers in `StreamMessageInput`.
- Added `StreamMessageInput.customAutocompleteTriggers` to allow users to define their custom
triggers.
## 5.0.0-beta.1
- 🎉 Initial support for desktop 🖥️ and web 🧑‍💻
- Right-click context menus for messages and full-screen attachments
- Upload and download attachments using the native desktop file system
- Press the "enter" key to send a message
- If you are quoting a message and have not yet typed any text, you can press the "esc" key to
remove the quoted message.
- A dedicated "X" button for removing a quoted message with your mouse
- Drag and drop attachment files to `StreamMessageInput`
- New `StreamMessageInput.draggingBorder` property to customize the border color of the
message input when dropping a file.
- Message reactions bubbles
- Hovering over a message reaction will show the users that have reacted to the message
- Desktop attachment sharing UI
- Selectable message text
- Gallery navigation controls with keyboard shortcuts (left and right arrow keys)
- Appropriate message sizing for large screens
- Right-click context menu for `StreamMessageListView` items
- `StreamMessageListView` items not swipeable on desktop & web
- Video support for Windows & Linux through `dart_vlc`
- Video support for macOS through `video_player_macos`
- Replace bottom sheets with dialogs where appropriate
- Other Additions ✅
- `onQuotedMessageCleared` to `StreamMessageInput`
- `selected` and `selectedTileColor` to `StreamChannelListTile`
- `AttachmentUploadStateBuilder.inProgressBuilder` to `AttachmentUploadStateBuilder`
- `AttachmentUploadStateBuilder.successBuilder` to `AttachmentUploadStateBuilder`
- `AttachmentUploadStateBuilder.failedBuilder` to `AttachmentUploadStateBuilder`
- Translations:
- `couldNotReadBytesFromFileError`
- `downloadLabel`
- `toggleMuteUnmuteAction`
- `toggleMuteUnmuteGroupQuestion`
- `toggleMuteUnmuteGroupText`
- `toggleMuteUnmuteUserQuestion`
- `toggleMuteUnmuteUserText`
- Deprecated `showConfirmationDialog` in favor of `showConfirmationBottomSheet`
- Deprecated `showInfoDialog` in favor of `showInfoBottomSheet`
- Deprecated `wrapAttachmentWidget` in favor of the `WrapAttachmentWidget` class
- Breaking changes 🚧
- `StreamImageAttachment.size` has been converted from type `Size` to type `BoxConstraints`
- `StreamFileAttachment.size` has been converted from type `Size` to type `BoxConstraints`
- `StreamGiphyAttachment.size` has been converted from type `Size` to type `BoxConstraints`
- `StreamVideoAttachment.size` has been converted from type `Size` to type `BoxConstraints`
- `StreamVideoThumbnailImage.width` and `StreamVideoThumbnailImage.height` have been removed in
favor of
`StreamVideoThumbnailImage.constraints`
- Dependency updates ⬆️
- `chewie: ^1.3.0` -> `chewie: ^1.3.4`
- `path_provider: ^2.0.1` -> `path_provider: ^2.0.9`
- `video_player: ^2.1.0` -> `video_player: ^2.4.5`
- Code Improvements 🔧
- Extracted many widgets to classes to improve readability, maintainability, and devtools usage.
- Organized internal directory structure
- Extracted typedefs to their own file
- Updated dartdoc documentation
- Various code readability improvements
## 4.6.0
🐞 Fixed
- [[#1323]](https://github.com/GetStream/stream-chat-flutter/issues/1323): Fix message text hiding
because of a [flutter bug](https://github.com/flutter/flutter/issues/110628).
## 4.5.0
- Updated `stream_chat_flutter_core` dependency
to [`4.5.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
🐞 Fixed
- [[#882]](https://github.com/GetStream/stream-chat-flutter/issues/882) Lots of unhandled exceptions
when network is off or spotty.
- Fixes an error where Stream CDN images were not being resized in the message list view.
🚀 Improved
- Automatically resize images that are above a specific pixel count to ensure resizing works:
getstream.io/chat/docs/go-golang/file_uploads/#image-resizing
✅ Added
- Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params
to `StreamMessageWidget` to customize the appearance of image attachment thumbnails.
```dart
StreamMessageListView(
messageBuilder: (context, details, messages, defaultMessage) {
return defaultMessage.copyWith(
imageAttachmentThumbnailSize: ...,
imageAttachmentThumbnailCropType: ...,
imageAttachmentThumbnailResizeType: ...,
);
},
),
```
- Added `thumbnailSize`, `thumbnailFormat`, `thumbnailQuality` and `thumbnailScale` params
to `StreamAttachmentPicker` to customize the appearance of image attachment thumbnails.
```dart
StreamMessageInput(
focusNode: _focusNode,
messageInputController: _messageInputController,
attachmentsPickerBuilder: (_, __, picker) {
return picker.copyWith(
attachmentThumbnailSize: ...,
attachmentThumbnailFormat: ...,
attachmentThumbnailQuality: ...,
attachmentThumbnailScale: ...,
);
},
),
```
## 4.4.1
🐞 Fixed
- [[#1247]](https://github.com/GetStream/stream-chat-flutter/issues/1247) Fix Jiffy initialization.
- [[#1232]](https://github.com/getstream/stream-chat-flutter/issues/1232) Fix DateDivider not
showing up in the chat.
- [[#1240]](https://github.com/getstream/stream-chat-flutter/issues/1240) Substitute mentioned user
ids with user names in system message.
- [[#1228]](https://github.com/GetStream/stream-chat-flutter/issues/1228) Fix image download on iOS.
🔄 Changed
- Changed default maximum attachment size from 20MB to 100MB.
## 4.4.0
🐞 Fixed
- [[#1234]](https://github.com/GetStream/stream-chat-flutter/issues/1234) Fix `ChannelListTile`
sendingIndicator `isMessageRead` calculation.
## 4.3.0
- Updated `photo_view` dependency to [`0.14.0`](https://pub.dev/packages/photo_view/changelog).
@@ -9,7 +196,8 @@
- [[#996]](https://github.com/GetStream/stream-chat-flutter/issues/996) Videos break bottom photo
carousal.
- Fix: URLs with path and/or query params are not enriched.
- [[#1194]](https://github.com/GetStream/stream-chat-flutter/issues/1194) Request permission to access gallery when opening the file picker.
- [[#1194]](https://github.com/GetStream/stream-chat-flutter/issues/1194) Request permission to
access gallery when opening the file picker.
✅ Added
+20
View File
@@ -60,6 +60,26 @@ We also use [video_player](https://pub.dev/packages/video_player) to reproduce v
To pick images from the camera, we use the [image_picker](https://pub.dev/packages/image_picker) plugin.
Follow [these instructions](https://pub.dev/packages/image_picker#ios) to check the requirements.
### Web
For the web, edit your `index.html` and add the following in the `<body>` tag in order to allow the SDK to override the right-click behaviour:
```html
<body oncontextmenu="return false;">
```
### MacOS
For MacOS use the [file_selector](https://pub.dev/packages/file_selector#macos) package.
Follow [these instructions](https://pub.dev/packages/file_selector#macos) to check the requirements.
You also need to add the following [entitlement](https://docs.flutter.dev/development/platform-integration/desktop#entitlements-and-the-app-sandbox):
```xml
<key>com.apple.security.network.client</key>
<true/>
```
### Troubleshooting
It may happen that you have some problems building the app.
@@ -32,7 +32,6 @@
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
@@ -40,7 +40,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example"
minSdkVersion 22
minSdkVersion 23
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -6,6 +6,9 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:name=".Application"
android:label="example"
@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>
@@ -339,7 +339,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -425,7 +425,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -474,7 +474,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -1,15 +1,18 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:responsive_builder/responsive_builder.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
void main() async {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
/// Create a new instance of [StreamChatClient] passing the apikey obtained
/// from your project dashboard.
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
logLevel: Level.OFF,
);
/// Set the current user and connect the websocket. In a production
@@ -63,50 +66,272 @@ class MyApp extends StatelessWidget {
final Channel channel;
@override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
supportedLocales: const [
Locale('en'),
Locale('hi'),
Locale('fr'),
Locale('it'),
Locale('es'),
],
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
builder: (context, widget) => StreamChat(
client: client,
child: widget,
),
home: StreamChannel(
channel: channel,
child: const ChannelPage(),
),
);
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
// themeMode: ThemeMode.dark,
supportedLocales: const [
Locale('en'),
Locale('hi'),
Locale('fr'),
Locale('it'),
Locale('es'),
],
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
builder: (context, widget) => StreamChat(
client: client,
child: widget,
),
home: StreamChannel(
channel: channel,
child: const ResponsiveChat(),
),
);
}
}
/// A list of messages sent in the current channel.
///
/// This is implemented using [StreamMessageListView],
/// a widget that provides query
/// functionalities fetching the messages from the api and showing them in a
/// listView.
class ChannelPage extends StatelessWidget {
/// Creates the page that shows the list of messages
const ChannelPage({
class ResponsiveChat extends StatelessWidget {
const ResponsiveChat({
super.key,
});
@override
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (context, sizingInformation) {
if (sizingInformation.isMobile) {
return ChannelListPage(
onTap: (c) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return StreamChannel(
channel: c,
child: ChannelPage(
onBackPressed: (context) {
Navigator.of(
context,
rootNavigator: true,
).pop();
},
),
);
},
),
);
},
);
}
return const SplitView();
},
breakpoints: const ScreenBreakpoints(
desktop: 550,
tablet: 550,
watch: 300,
),
);
}
}
class SplitView extends StatefulWidget {
const SplitView({
super.key,
});
@override
_SplitViewState createState() => _SplitViewState();
}
class _SplitViewState extends State<SplitView> {
Channel? selectedChannel;
@override
Widget build(BuildContext context) {
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
child: ChannelListPage(
onTap: (channel) {
setState(() {
selectedChannel = channel;
});
},
selectedChannel: selectedChannel,
),
),
Flexible(
flex: 2,
child: ClipPath(
child: Scaffold(
body: selectedChannel != null
? StreamChannel(
key: ValueKey(selectedChannel!.cid),
channel: selectedChannel!,
child: const ChannelPage(showBackButton: false),
)
: Center(
child: Text(
'Pick a channel to show the messages 💬',
style: Theme.of(context).textTheme.headline5,
),
),
),
StreamMessageInput(attachmentLimit: 3),
],
),
),
],
);
}
}
class ChannelListPage extends StatefulWidget {
const ChannelListPage({
super.key,
this.onTap,
this.selectedChannel,
});
final void Function(Channel)? onTap;
final Channel? selectedChannel;
@override
State<ChannelListPage> createState() => _ChannelListPageState();
}
class _ChannelListPageState extends State<ChannelListPage> {
late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
limit: 20,
);
@override
Widget build(BuildContext context) => Scaffold(
body: StreamChannelListView(
onChannelTap: widget.onTap,
controller: _listController,
itemBuilder: (context, channels, index, defaultWidget) {
return defaultWidget.copyWith(
selected: channels[index] == widget.selectedChannel,
);
},
),
);
}
class ChannelPage extends StatefulWidget {
const ChannelPage({
super.key,
this.showBackButton = true,
this.onBackPressed,
});
final bool showBackButton;
final void Function(BuildContext)? onBackPressed;
@override
State<ChannelPage> createState() => _ChannelPageState();
}
class _ChannelPageState extends State<ChannelPage> {
late final messageInputController = StreamMessageInputController();
final focusNode = FocusNode();
@override
Widget build(BuildContext context) => Navigator(
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => Scaffold(
appBar: StreamChannelHeader(
onBackPressed: widget.onBackPressed != null
? () {
widget.onBackPressed!(context);
}
: null,
showBackButton: widget.showBackButton,
),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
onMessageSwiped:
(CurrentPlatform.isAndroid || CurrentPlatform.isIos)
? reply
: null,
threadBuilder: (context, parent) {
return ThreadPage(
parent: parent!,
);
},
messageBuilder:
(context, details, messages, defaultWidget) {
return defaultWidget.copyWith(
onReplyTap: reply,
);
},
),
),
StreamMessageInput(
onQuotedMessageCleared:
messageInputController.clearQuotedMessage,
focusNode: focusNode,
messageInputController: messageInputController,
),
],
),
),
),
);
void reply(Message message) {
messageInputController.quotedMessage = message;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
focusNode.requestFocus();
});
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
}
class ThreadPage extends StatelessWidget {
const ThreadPage({
super.key,
required this.parent,
});
final Message parent;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: StreamThreadHeader(
parent: parent,
),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
parentMessage: parent,
),
),
StreamMessageInput(
messageInputController: StreamMessageInputController(
message: Message(parentId: parent.id),
),
),
],
),
);
}
}
@@ -2,7 +2,7 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
void main() async {
Future<void> main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
@@ -29,13 +29,15 @@ class MyApp extends StatelessWidget {
final StreamChatClient client;
@override
Widget build(BuildContext context) => MaterialApp(
builder: (context, child) => StreamChat(
client: client,
child: child,
),
home: const SplitView(),
);
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => StreamChat(
client: client,
child: child,
),
home: const SplitView(),
);
}
}
class SplitView extends StatefulWidget {
@@ -51,37 +53,39 @@ class _SplitViewState extends State<SplitView> {
Channel? selectedChannel;
@override
Widget build(BuildContext context) => Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
child: ChannelListPage(
onTap: (channel) {
setState(() {
selectedChannel = channel;
});
},
),
Widget build(BuildContext context) {
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
child: ChannelListPage(
onTap: (channel) {
setState(() {
selectedChannel = channel;
});
},
),
Flexible(
flex: 2,
child: Scaffold(
body: selectedChannel != null
? StreamChannel(
key: ValueKey(selectedChannel!.cid),
channel: selectedChannel!,
child: const ChannelPage(),
)
: Center(
child: Text(
'Pick a channel to show the messages 💬',
style: Theme.of(context).textTheme.headline5,
),
),
Flexible(
flex: 2,
child: Scaffold(
body: selectedChannel != null
? StreamChannel(
key: ValueKey(selectedChannel!.cid),
channel: selectedChannel!,
child: const ChannelPage(),
)
: Center(
child: Text(
'Pick a channel to show the messages 💬',
style: Theme.of(context).textTheme.headline5,
),
),
),
),
],
);
),
],
);
}
}
class ChannelListPage extends StatefulWidget {
@@ -1,5 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -32,7 +31,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// and [StreamMessageInput]
///
/// If you now run the simulator you will see a single channel UI.
void main() async {
Future<void> main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
@@ -90,15 +89,18 @@ class ChannelPage extends StatelessWidget {
});
@override
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
),
StreamMessageInput(),
],
),
);
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
),
StreamMessageInput(),
],
),
);
}
}
@@ -1,6 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -31,7 +29,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// [StreamChannelListView] handles pagination
/// and updates automatically when new channels are created or when a new
/// message is added to a channel.
void main() async {
Future<void> main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
@@ -100,23 +98,25 @@ class _ChannelListPageState extends State<ChannelListPage> {
}
@override
Widget build(BuildContext context) => Scaffold(
body: RefreshIndicator(
onRefresh: _controller.refresh,
child: StreamChannelListView(
controller: _controller,
onChannelTap: (channel) => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: _controller.refresh,
child: StreamChannelListView(
controller: _controller,
onChannelTap: (channel) => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
),
),
),
);
),
);
}
}
class ChannelPage extends StatelessWidget {
@@ -125,15 +125,17 @@ class ChannelPage extends StatelessWidget {
});
@override
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
),
StreamMessageInput(),
],
),
);
Widget build(BuildContext context) {
return Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
),
StreamMessageInput(),
],
),
);
}
}
@@ -1,5 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -1,6 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -106,21 +104,23 @@ class ChannelPage extends StatelessWidget {
});
@override
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
Widget build(BuildContext context) {
return Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
),
const StreamMessageInput(),
],
),
);
),
const StreamMessageInput(),
],
),
);
}
}
class ThreadPage extends StatelessWidget {
@@ -1,5 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -86,21 +85,23 @@ class _ChannelListPageState extends State<ChannelListPage> {
}
@override
Widget build(BuildContext context) => Scaffold(
body: StreamChannelListView(
controller: _listController,
onChannelTap: (channel) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
Widget build(BuildContext context) {
return Scaffold(
body: StreamChannelListView(
controller: _listController,
onChannelTap: (channel) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
);
},
),
);
),
);
},
),
);
}
}
class ChannelPage extends StatelessWidget {
@@ -1,6 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -126,21 +124,23 @@ class _ChannelListPageState extends State<ChannelListPage> {
}
@override
Widget build(BuildContext context) => Scaffold(
body: StreamChannelListView(
controller: _listController,
onChannelTap: (channel) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
Widget build(BuildContext context) {
return Scaffold(
body: StreamChannelListView(
controller: _listController,
onChannelTap: (channel) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
);
},
),
);
),
);
},
),
);
}
}
class ChannelPage extends StatelessWidget {
@@ -149,21 +149,23 @@ class ChannelPage extends StatelessWidget {
});
@override
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
Widget build(BuildContext context) {
return Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
),
const StreamMessageInput(),
],
),
);
),
const StreamMessageInput(),
],
),
);
}
}
class ThreadPage extends StatelessWidget {
@@ -0,0 +1 @@
flutter/ephemeral
@@ -0,0 +1,116 @@
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
set(BINARY_NAME "example")
set(APPLICATION_ID "com.example.example")
cmake_policy(SET CMP0063 NEW)
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Configure build options.
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()
# Compilation settings that should be applied to most targets.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
# Flutter library and tool build rules.
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Application build
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
apply_standard_settings(${BINARY_NAME})
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
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.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
@@ -0,0 +1,87 @@
cmake_minimum_required(VERSION 3.10)
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.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# 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/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter 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.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)
@@ -0,0 +1,35 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <dart_vlc/dart_vlc_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dart_vlc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DartVlcPlugin");
dart_vlc_plugin_register_with_registrar(dart_vlc_registrar);
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
}
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
@@ -0,0 +1,29 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
dart_vlc
desktop_drop
screen_retriever
sqlite3_flutter_libs
url_launcher_linux
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux 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)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}
@@ -0,0 +1,104 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "example");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "example");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}
@@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_
@@ -10,5 +10,9 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
@@ -28,5 +28,7 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>In order to access your photo library</string>
</dict>
</plist>
@@ -4,12 +4,12 @@ import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}
}
@@ -24,9 +24,10 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
collection: ^1.15.0
cupertino_icons: ^1.0.3
cupertino_icons: ^1.0.4
flutter:
sdk: flutter
responsive_builder: ^0.4.2
stream_chat_flutter:
path: ../
stream_chat_localizations:
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -11,7 +11,6 @@
Fore more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
-->
<base href="/">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
@@ -31,7 +30,7 @@
<title>example</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<body oncontextmenu="return false;">
<!-- 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 -->
@@ -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,41 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <connectivity_plus_windows/connectivity_plus_windows_plugin.h>
#include <dart_vlc/dart_vlc_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_native_view/flutter_native_view_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <thumblr_windows/thumblr_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
DartVlcPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DartVlcPlugin"));
DesktopDropPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterNativeViewPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterNativeViewPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
ThumblrWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ThumblrWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}
@@ -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,33 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus_windows
dart_vlc
desktop_drop
file_selector_windows
flutter_native_view
screen_retriever
sqlite3_flutter_libs
thumblr_windows
url_launcher_windows
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
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)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_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", "com.example" "\0"
VALUE "FileDescription", "example" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "example" "\0"
VALUE "LegalCopyright", "Copyright (C) 2022 com.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_
@@ -0,0 +1,25 @@
# conditional_parent_builder
A widget that allows developers to conditionally wrap a child widget with a parent widget.
This is useful in situations where a child widget should always be built, but should be wrapped
by another widget if certain conditions are met.
In the following real-world example, we conditionally wrap the child widget
tree with a context menu if the message is not deleted:
```dart
ConditionalParentBuilder(
builder: (context, child) {
if (!widget.message.isDeleted) {
return ContextMenuArea(
builder: (context) => buildContextMenu(),
child: child,
);
} else {
return child;
}
},
child: Material(...),
),
```
This example can be found in the `stream_chat_flutter` source code under
`src/message_widget/message_widget.dart`.

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