Merge branch 'develop'
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
name: Dart Code Metrics
|
name: Dart Code Metrics
|
||||||
|
|
||||||
env:
|
env:
|
||||||
flutter_version: "3.0.0"
|
flutter_version: "3.3.3"
|
||||||
folders: "lib, test"
|
folders: "lib, test"
|
||||||
melos_version: "2.1.0"
|
melos_version: "2.7.1"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ name: stream_flutter_workflow
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
|
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
|
||||||
flutter_version: "3.0.0"
|
flutter_version: "3.3.3"
|
||||||
melos_version: "2.1.0"
|
melos_version: "2.7.1"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
if: github.event.pull_request.draft == false
|
if: github.event.pull_request.draft == false
|
||||||
timeout-minutes: 20
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: "Git Checkout"
|
- name: "Git Checkout"
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -111,22 +111,27 @@ jobs:
|
|||||||
uses: VeryGoodOpenSource/[email protected]
|
uses: VeryGoodOpenSource/[email protected]
|
||||||
with:
|
with:
|
||||||
path: packages/stream_chat/coverage/lcov.info
|
path: packages/stream_chat/coverage/lcov.info
|
||||||
min_coverage: 80
|
min_coverage: 79
|
||||||
|
- name: "Stream Chat Persistence Coverage Check"
|
||||||
|
uses: VeryGoodOpenSource/[email protected]
|
||||||
|
with:
|
||||||
|
path: packages/stream_chat_localizations/coverage/lcov.info
|
||||||
|
min_coverage: 88
|
||||||
- name: "Stream Chat Persistence Coverage Check"
|
- name: "Stream Chat Persistence Coverage Check"
|
||||||
uses: VeryGoodOpenSource/[email protected]
|
uses: VeryGoodOpenSource/[email protected]
|
||||||
with:
|
with:
|
||||||
path: packages/stream_chat_persistence/coverage/lcov.info
|
path: packages/stream_chat_persistence/coverage/lcov.info
|
||||||
min_coverage: 95
|
min_coverage: 97
|
||||||
- name: "Stream Chat Flutter Core Coverage Check"
|
- name: "Stream Chat Flutter Core Coverage Check"
|
||||||
uses: VeryGoodOpenSource/[email protected]
|
uses: VeryGoodOpenSource/[email protected]
|
||||||
with:
|
with:
|
||||||
path: packages/stream_chat_flutter_core/coverage/lcov.info
|
path: packages/stream_chat_flutter_core/coverage/lcov.info
|
||||||
min_coverage: 90
|
min_coverage: 30
|
||||||
- name: "Stream Chat Flutter Coverage Check"
|
- name: "Stream Chat Flutter Coverage Check"
|
||||||
uses: VeryGoodOpenSource/[email protected]
|
uses: VeryGoodOpenSource/[email protected]
|
||||||
with:
|
with:
|
||||||
path: packages/stream_chat_flutter/coverage/lcov.info
|
path: packages/stream_chat_flutter/coverage/lcov.info
|
||||||
min_coverage: 67
|
min_coverage: 44
|
||||||
|
|
||||||
draft-build:
|
draft-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ analyzer:
|
|||||||
exclude:
|
exclude:
|
||||||
- packages/*/lib/**/*.g.dart
|
- packages/*/lib/**/*.g.dart
|
||||||
- packages/*/example/**
|
- packages/*/example/**
|
||||||
- packages/*/lib/src/emoji
|
|
||||||
- packages/*/lib/**/*.freezed.dart
|
- packages/*/lib/**/*.freezed.dart
|
||||||
- packages/*/test/**
|
- packages/*/test/**
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ analyzer:
|
|||||||
- dart_code_metrics
|
- dart_code_metrics
|
||||||
exclude:
|
exclude:
|
||||||
- packages/*/lib/**/*.g.dart
|
- packages/*/lib/**/*.g.dart
|
||||||
- packages/*/lib/src/emoji/**
|
|
||||||
- packages/*/lib/scrollable_positioned_list/**
|
- packages/*/lib/scrollable_positioned_list/**
|
||||||
- packages/*/lib/**/*.freezed.dart
|
- 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
@@ -1,9 +1,6 @@
|
|||||||
name: stream_chat_flutter
|
name: stream_chat_flutter
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
|
|
||||||
versioning:
|
|
||||||
mode: independent
|
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
- packages/**
|
- packages/**
|
||||||
|
|
||||||
@@ -54,11 +51,11 @@ scripts:
|
|||||||
description: Build all generated files for Dart & Flutter packages in this project.
|
description: Build all generated files for Dart & Flutter packages in this project.
|
||||||
|
|
||||||
generate:dart:
|
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.
|
description: Build all generated files for Dart packages in this project.
|
||||||
|
|
||||||
generate:flutter:
|
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.
|
description: Build all generated files for Flutter packages in this project.
|
||||||
|
|
||||||
test:all:
|
test:all:
|
||||||
@@ -97,10 +94,3 @@ scripts:
|
|||||||
npm install -g https://github.com/GetStream/stream-chat-docusaurus-cli &&
|
npm install -g https://github.com/GetStream/stream-chat-docusaurus-cli &&
|
||||||
npx stream-chat-docusaurus -i -s
|
npx stream-chat-docusaurus -i -s
|
||||||
description: Runs the docusaurus documentation locally.
|
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'
|
|
||||||
@@ -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
|
## 4.3.0
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import 'package:stream_chat/stream_chat.dart';
|
|||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
/// Create a new instance of [StreamChatClient]
|
/// Create a new instance of [StreamChatClient]
|
||||||
/// by passing the apikey obtained from your project dashboard.
|
/// 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
|
/// Set the current user. In a production scenario, this should be done using
|
||||||
/// a backend to generate a user token using our server SDK.
|
/// a backend to generate a user token using our server SDK.
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart'
|
import 'package:collection/collection.dart'
|
||||||
show IterableExtension, ListEquality;
|
show IterableExtension, ListEquality;
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:stream_chat/src/client/retry_queue.dart';
|
import 'package:stream_chat/src/client/retry_queue.dart';
|
||||||
import 'package:stream_chat/src/core/util/utils.dart';
|
import 'package:stream_chat/src/core/util/utils.dart';
|
||||||
import 'package:stream_chat/stream_chat.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.
|
/// Class that manages a specific channel.
|
||||||
///
|
///
|
||||||
/// #### Channel name
|
/// #### Channel name
|
||||||
@@ -1061,9 +1064,9 @@ class Channel {
|
|||||||
/// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart
|
/// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart
|
||||||
/// for more information.
|
/// for more information.
|
||||||
Future<UpdateChannelResponse> update(
|
Future<UpdateChannelResponse> update(
|
||||||
Map<String, Object?> channelData, [
|
Map<String, Object?> channelData, {
|
||||||
Message? updateMessage,
|
Message? updateMessage,
|
||||||
]) async {
|
}) async {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
return _client.updateChannel(
|
return _client.updateChannel(
|
||||||
id!,
|
id!,
|
||||||
@@ -1146,27 +1149,34 @@ class Channel {
|
|||||||
|
|
||||||
/// Add members to the channel.
|
/// Add members to the channel.
|
||||||
Future<AddMembersResponse> addMembers(
|
Future<AddMembersResponse> addMembers(
|
||||||
List<String> memberIds, [
|
List<String> memberIds, {
|
||||||
Message? message,
|
Message? message,
|
||||||
]) async {
|
bool hideHistory = false,
|
||||||
|
}) async {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
return _client.addChannelMembers(id!, type, memberIds, message: message);
|
return _client.addChannelMembers(
|
||||||
|
id!,
|
||||||
|
type,
|
||||||
|
memberIds,
|
||||||
|
message: message,
|
||||||
|
hideHistory: hideHistory,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invite members to the channel.
|
/// Invite members to the channel.
|
||||||
Future<InviteMembersResponse> inviteMembers(
|
Future<InviteMembersResponse> inviteMembers(
|
||||||
List<String> memberIds, [
|
List<String> memberIds, {
|
||||||
Message? message,
|
Message? message,
|
||||||
]) async {
|
}) async {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
return _client.inviteChannelMembers(id!, type, memberIds, message: message);
|
return _client.inviteChannelMembers(id!, type, memberIds, message: message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove members from the channel.
|
/// Remove members from the channel.
|
||||||
Future<RemoveMembersResponse> removeMembers(
|
Future<RemoveMembersResponse> removeMembers(
|
||||||
List<String> memberIds, [
|
List<String> memberIds, {
|
||||||
Message? message,
|
Message? message,
|
||||||
]) async {
|
}) async {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
return _client.removeChannelMembers(id!, type, memberIds, message: message);
|
return _client.removeChannelMembers(id!, type, memberIds, message: message);
|
||||||
}
|
}
|
||||||
@@ -1494,36 +1504,38 @@ class Channel {
|
|||||||
)
|
)
|
||||||
.where((e) => e.cid == cid);
|
.where((e) => e.cid == cid);
|
||||||
|
|
||||||
DateTime? _lastTypingEvent;
|
late final _keyStrokeHandler = KeyStrokeHandler(
|
||||||
|
onStartTyping: startTyping,
|
||||||
|
onStopTyping: stopTyping,
|
||||||
|
);
|
||||||
|
|
||||||
/// First of the [EventType.typingStart] and [EventType.typingStop] events
|
/// Sends the [Event.typingStart] event and schedules a timer to invoke the
|
||||||
/// based on the users keystrokes. Call this on every keystroke.
|
/// [Event.typingStop] event.
|
||||||
|
///
|
||||||
|
/// This is meant to be called every time the user presses a key.
|
||||||
Future<void> keyStroke([String? parentId]) async {
|
Future<void> keyStroke([String? parentId]) async {
|
||||||
if (config?.typingEvents == false) {
|
if (config?.typingEvents == false) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
client.logger.info('start typing');
|
client.logger.info('KeyStroke received');
|
||||||
final now = DateTime.now();
|
return _keyStrokeHandler(parentId);
|
||||||
|
|
||||||
if (_lastTypingEvent == null ||
|
|
||||||
now.difference(_lastTypingEvent!).inSeconds >= 2) {
|
|
||||||
_lastTypingEvent = now;
|
|
||||||
await sendEvent(Event(
|
|
||||||
type: EventType.typingStart,
|
|
||||||
parentId: 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 {
|
Future<void> stopTyping([String? parentId]) async {
|
||||||
if (config?.typingEvents == false) {
|
if (config?.typingEvents == false) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
client.logger.info('stop typing');
|
client.logger.info('stop typing');
|
||||||
_lastTypingEvent = null;
|
|
||||||
await sendEvent(Event(
|
await sendEvent(Event(
|
||||||
type: EventType.typingStop,
|
type: EventType.typingStop,
|
||||||
parentId: parentId,
|
parentId: parentId,
|
||||||
@@ -1533,6 +1545,7 @@ class Channel {
|
|||||||
/// Call this method to dispose the channel client.
|
/// Call this method to dispose the channel client.
|
||||||
void dispose() {
|
void dispose() {
|
||||||
state?.dispose();
|
state?.dispose();
|
||||||
|
_keyStrokeHandler.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _checkInitialized() {
|
void _checkInitialized() {
|
||||||
@@ -1594,9 +1607,9 @@ class ChannelClientState {
|
|||||||
|
|
||||||
_listenMemberUnbanned();
|
_listenMemberUnbanned();
|
||||||
|
|
||||||
_startCleaning();
|
_startCleaningStaleTypingEvents();
|
||||||
|
|
||||||
_startCleaningPinnedMessages();
|
_startCleaningStalePinnedMessages();
|
||||||
|
|
||||||
_channel._client.chatPersistenceClient
|
_channel._client.chatPersistenceClient
|
||||||
?.getChannelThreads(_channel.cid!)
|
?.getChannelThreads(_channel.cid!)
|
||||||
@@ -1614,7 +1627,8 @@ class ChannelClientState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _subscriptions = <StreamSubscription>[];
|
final Channel _channel;
|
||||||
|
final _subscriptions = CompositeSubscription();
|
||||||
|
|
||||||
void _checkExpiredAttachmentMessages(ChannelState channelState) async {
|
void _checkExpiredAttachmentMessages(ChannelState channelState) async {
|
||||||
final expiredAttachmentMessagesId = channelState.messages
|
final expiredAttachmentMessagesId = channelState.messages
|
||||||
@@ -1787,20 +1801,15 @@ class ChannelClientState {
|
|||||||
|
|
||||||
/// Retry failed message.
|
/// Retry failed message.
|
||||||
Future<void> retryFailedMessages() async {
|
Future<void> retryFailedMessages() async {
|
||||||
final failedMessages =
|
final failedMessages = [...messages, ...threads.values.expand((v) => v)]
|
||||||
<Message>[...messages, ...threads.values.expand((v) => v)]
|
.where(
|
||||||
.where(
|
(message) =>
|
||||||
(message) =>
|
message.status != MessageSendingStatus.sent &&
|
||||||
message.status != MessageSendingStatus.sent &&
|
message.createdAt.isBefore(
|
||||||
message.createdAt.isBefore(
|
DateTime.now().subtract(const Duration(seconds: 5)),
|
||||||
DateTime.now().subtract(
|
),
|
||||||
const Duration(
|
)
|
||||||
seconds: 5,
|
.toList();
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
_retryQueue.add(failedMessages);
|
_retryQueue.add(failedMessages);
|
||||||
}
|
}
|
||||||
@@ -1956,11 +1965,13 @@ class ChannelClientState {
|
|||||||
// Early return in case the thread is not available
|
// Early return in case the thread is not available
|
||||||
if (!newThreads.containsKey(parentId)) return;
|
if (!newThreads.containsKey(parentId)) return;
|
||||||
|
|
||||||
_threads = newThreads
|
// Remove thread message shown in thread page.
|
||||||
..update(
|
newThreads.update(
|
||||||
parentId,
|
parentId,
|
||||||
(messages) => messages..removeWhere((e) => e.id == message.id),
|
(messages) => [...messages.where((e) => e.id != message.id)],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_threads = newThreads;
|
||||||
|
|
||||||
// Early return if the thread message is not shown in channel.
|
// Early return if the thread message is not shown in channel.
|
||||||
if (message.showInChannel == false) return;
|
if (message.showInChannel == false) return;
|
||||||
@@ -1985,12 +1996,7 @@ class ChannelClientState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_subscriptions.add(
|
_subscriptions.add(
|
||||||
_channel
|
_channel.on(EventType.messageRead, EventType.notificationMarkRead).listen(
|
||||||
.on(
|
|
||||||
EventType.messageRead,
|
|
||||||
EventType.notificationMarkRead,
|
|
||||||
)
|
|
||||||
.listen(
|
|
||||||
(event) {
|
(event) {
|
||||||
final readList = List<Read>.from(_channelState.read ?? []);
|
final readList = List<Read>.from(_channelState.read ?? []);
|
||||||
final userReadIndex =
|
final userReadIndex =
|
||||||
@@ -2078,10 +2084,6 @@ class ChannelClientState {
|
|||||||
(m) => m.user?.id == _channel.client.state.currentUser?.id,
|
(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
|
/// Channel role for the current user
|
||||||
String? get currentUserChannelRole => currentUserMember?.channelRole;
|
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.
|
/// Channel related typing users stream.
|
||||||
Stream<Map<User, Event>> get typingEventsStream =>
|
Stream<Map<User, Event>> get typingEventsStream =>
|
||||||
_typingEventsController.stream;
|
_typingEventsController.stream;
|
||||||
|
|
||||||
final BehaviorSubject<Map<User, Event>> _typingEventsController =
|
/// Channel related typing users last value.
|
||||||
BehaviorSubject.seeded({});
|
Map<User, Event> get typingEvents => _typingEventsController.value;
|
||||||
|
final _typingEventsController = BehaviorSubject.seeded(<User, Event>{});
|
||||||
final Channel _channel;
|
|
||||||
final Map<User, Event> _typings = {};
|
|
||||||
|
|
||||||
void _listenTypingEvents() {
|
void _listenTypingEvents() {
|
||||||
if (_channelState.channel?.config.typingEvents == false) {
|
if (_channelState.channel?.config.typingEvents == false) return;
|
||||||
return;
|
|
||||||
}
|
final currentUser = _channel.client.state.currentUser;
|
||||||
|
if (currentUser == null) return;
|
||||||
|
|
||||||
_subscriptions
|
_subscriptions
|
||||||
..add(
|
..add(
|
||||||
_channel.on(EventType.typingStart).listen(
|
_channel.on(EventType.typingStart).listen(
|
||||||
(event) {
|
(event) {
|
||||||
if (event.user != null) {
|
final user = event.user;
|
||||||
final user = event.user!;
|
if (user != null && user.id != currentUser.id) {
|
||||||
if (user.id != _channel.client.state.currentUser?.id) {
|
final events = {...typingEvents};
|
||||||
_typings[user] = event;
|
events[user] = event;
|
||||||
_typingEventsController.add(_typings);
|
_typingEventsController.add(events);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -2286,112 +2283,109 @@ class ChannelClientState {
|
|||||||
..add(
|
..add(
|
||||||
_channel.on(EventType.typingStop).listen(
|
_channel.on(EventType.typingStop).listen(
|
||||||
(event) {
|
(event) {
|
||||||
if (event.user != null) {
|
final user = event.user;
|
||||||
final user = event.user!;
|
if (user != null && user.id != currentUser.id) {
|
||||||
if (user.id != _channel.client.state.currentUser?.id) {
|
final events = {...typingEvents}..remove(user);
|
||||||
_typings.remove(event.user);
|
_typingEventsController.add(events);
|
||||||
_typingEventsController.add(_typings);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
..add(
|
..add(
|
||||||
_channel
|
_channel.on().where((event) {
|
||||||
.on()
|
final user = event.user;
|
||||||
.where((event) =>
|
if (user == null) return false;
|
||||||
event.user != null &&
|
return members.any((m) => m.userId == user.id);
|
||||||
members.any((m) => m.userId == event.user!.id))
|
}).listen(
|
||||||
.listen(
|
|
||||||
(event) {
|
(event) {
|
||||||
final newMembers = List<Member>.from(members);
|
final newMembers = List<Member>.from(members);
|
||||||
final oldMemberIndex =
|
final oldMemberIndex =
|
||||||
newMembers.indexWhere((m) => m.userId == event.user!.id);
|
newMembers.indexWhere((m) => m.userId == event.user!.id);
|
||||||
if (oldMemberIndex > -1) {
|
if (oldMemberIndex > -1) {
|
||||||
final oldMember = newMembers.removeAt(oldMemberIndex);
|
final oldMember = newMembers.removeAt(oldMemberIndex);
|
||||||
updateChannelState(ChannelState(
|
updateChannelState(
|
||||||
members: [
|
ChannelState(
|
||||||
...newMembers,
|
members: [
|
||||||
oldMember.copyWith(
|
...newMembers,
|
||||||
user: event.user,
|
oldMember.copyWith(
|
||||||
),
|
user: event.user,
|
||||||
],
|
),
|
||||||
));
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer? _cleaningTimer;
|
Timer? _staleTypingEventsCleanerTimer;
|
||||||
|
|
||||||
void _startCleaning() {
|
// Checks and removes stale typing events that were not explicitly stopped by
|
||||||
if (_channelState.channel?.config.typingEvents == false) {
|
// the sender due to technical difficulties. e.g. process death, loss of
|
||||||
return;
|
// Internet connection or custom implementation.
|
||||||
}
|
void _startCleaningStaleTypingEvents() {
|
||||||
|
if (_channelState.channel?.config.typingEvents == false) return;
|
||||||
|
|
||||||
_cleaningTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
_staleTypingEventsCleanerTimer = Timer.periodic(
|
||||||
final now = DateTime.now();
|
const Duration(seconds: 1),
|
||||||
|
(_) {
|
||||||
if (_channel._lastTypingEvent != null &&
|
final now = DateTime.now();
|
||||||
now.difference(_channel._lastTypingEvent!).inSeconds > 1) {
|
typingEvents.forEach((user, event) {
|
||||||
_channel.stopTyping();
|
if (now.difference(event.createdAt).inSeconds >
|
||||||
}
|
incomingTypingStartEventTimeout) {
|
||||||
|
_channel.client.handleEvent(
|
||||||
_clean();
|
Event(
|
||||||
});
|
type: EventType.typingStop,
|
||||||
|
user: user,
|
||||||
|
cid: _channel.cid,
|
||||||
|
parentId: event.parentId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
late Timer _pinnedMessagesTimer;
|
Timer? _stalePinnedMessagesCleanerTimer;
|
||||||
|
|
||||||
void _startCleaningPinnedMessages() {
|
// Checks and removes stale pinned messages that are not valid anymore.
|
||||||
_pinnedMessagesTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
void _startCleaningStalePinnedMessages() {
|
||||||
final now = DateTime.now();
|
_stalePinnedMessagesCleanerTimer = Timer.periodic(
|
||||||
var expiredMessages = channelState.pinnedMessages
|
const Duration(seconds: 30),
|
||||||
?.where((m) => m.pinExpires?.isBefore(now) == true)
|
(_) {
|
||||||
.toList();
|
final now = DateTime.now();
|
||||||
if (expiredMessages != null && expiredMessages.isNotEmpty) {
|
var expiredMessages = channelState.pinnedMessages
|
||||||
expiredMessages = expiredMessages
|
?.where((m) => m.pinExpires?.isBefore(now) == true)
|
||||||
.map((m) => m.copyWith(
|
|
||||||
pinExpires: null,
|
|
||||||
pinned: false,
|
|
||||||
))
|
|
||||||
.toList();
|
.toList();
|
||||||
|
if (expiredMessages != null && expiredMessages.isNotEmpty) {
|
||||||
|
expiredMessages = expiredMessages
|
||||||
|
.map((m) => m.copyWith(
|
||||||
|
pinExpires: null,
|
||||||
|
pinned: false,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
|
||||||
updateChannelState(_channelState.copyWith(
|
updateChannelState(_channelState.copyWith(
|
||||||
pinnedMessages: pinnedMessages.where(_pinIsValid).toList(),
|
pinnedMessages: pinnedMessages.where(_pinIsValid).toList(),
|
||||||
messages: expiredMessages,
|
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call this method to dispose this object.
|
/// Call this method to dispose this object.
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_debouncedUpdatePersistenceChannelState.cancel();
|
_debouncedUpdatePersistenceChannelState.cancel();
|
||||||
_retryQueue.dispose();
|
_retryQueue.dispose();
|
||||||
_subscriptions.forEach((s) => s.cancel());
|
_subscriptions.cancel();
|
||||||
_channelStateController.close();
|
_channelStateController.close();
|
||||||
_isUpToDateController.close();
|
_isUpToDateController.close();
|
||||||
_threadsController.close();
|
_threadsController.close();
|
||||||
_cleaningTimer?.cancel();
|
_staleTypingEventsCleanerTimer?.cancel();
|
||||||
_pinnedMessagesTimer.cancel();
|
_stalePinnedMessagesCleanerTimer?.cancel();
|
||||||
_typingEventsController.close();
|
_typingEventsController.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ class StreamChatClient {
|
|||||||
Duration receiveTimeout = const Duration(seconds: 6),
|
Duration receiveTimeout = const Duration(seconds: 6),
|
||||||
StreamChatApi? chatApi,
|
StreamChatApi? chatApi,
|
||||||
WebSocket? ws,
|
WebSocket? ws,
|
||||||
AttachmentFileUploader? attachmentFileUploader,
|
AttachmentFileUploaderProvider attachmentFileUploaderProvider =
|
||||||
|
StreamAttachmentFileUploader.new,
|
||||||
}) {
|
}) {
|
||||||
logger.info('Initiating new StreamChatClient');
|
logger.info('Initiating new StreamChatClient');
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ class StreamChatClient {
|
|||||||
options: options,
|
options: options,
|
||||||
tokenManager: _tokenManager,
|
tokenManager: _tokenManager,
|
||||||
connectionIdManager: _connectionIdManager,
|
connectionIdManager: _connectionIdManager,
|
||||||
attachmentFileUploader: attachmentFileUploader,
|
attachmentFileUploaderProvider: attachmentFileUploaderProvider,
|
||||||
logger: detachedLogger('🕸️'),
|
logger: detachedLogger('🕸️'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -380,6 +381,10 @@ class StreamChatClient {
|
|||||||
user,
|
user,
|
||||||
includeUserDetails: includeUserDetailsInConnectCall,
|
includeUserDetails: includeUserDetailsInConnectCall,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Start listening to events
|
||||||
|
state.subscribeToEvents();
|
||||||
|
|
||||||
return user.merge(event.me);
|
return user.merge(event.me);
|
||||||
} catch (e, stk) {
|
} catch (e, stk) {
|
||||||
logger.severe('error connecting ws', e, stk);
|
logger.severe('error connecting ws', e, stk);
|
||||||
@@ -401,6 +406,9 @@ class StreamChatClient {
|
|||||||
_connectionStatusSubscription?.cancel();
|
_connectionStatusSubscription?.cancel();
|
||||||
_connectionStatusSubscription = null;
|
_connectionStatusSubscription = null;
|
||||||
|
|
||||||
|
// Stop listening to events
|
||||||
|
state.cancelEventSubscription();
|
||||||
|
|
||||||
_ws.disconnect();
|
_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.
|
/// Requests channels with a given query from the API.
|
||||||
Future<List<Channel>> queryChannelsOnline({
|
Future<List<Channel>> queryChannelsOnline({
|
||||||
Filter? filter,
|
Filter? filter,
|
||||||
@@ -1027,12 +1054,14 @@ class StreamChatClient {
|
|||||||
String channelType,
|
String channelType,
|
||||||
List<String> memberIds, {
|
List<String> memberIds, {
|
||||||
Message? message,
|
Message? message,
|
||||||
|
bool hideHistory = false,
|
||||||
}) =>
|
}) =>
|
||||||
_chatApi.channel.addMembers(
|
_chatApi.channel.addMembers(
|
||||||
channelId,
|
channelId,
|
||||||
channelType,
|
channelType,
|
||||||
memberIds,
|
memberIds,
|
||||||
message: message,
|
message: message,
|
||||||
|
hideHistory: hideHistory,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Remove members from the channel
|
/// Remove members from the channel
|
||||||
@@ -1452,29 +1481,40 @@ class StreamChatClient {
|
|||||||
/// The class that handles the state of the channel listening to the events
|
/// The class that handles the state of the channel listening to the events
|
||||||
class ClientState {
|
class ClientState {
|
||||||
/// Creates a new instance listening to events and updating the state
|
/// Creates a new instance listening to events and updating the state
|
||||||
ClientState(this._client) {
|
ClientState(this._client);
|
||||||
_subscriptions.addAll([
|
|
||||||
_client
|
CompositeSubscription? _eventsSubscription;
|
||||||
|
|
||||||
|
/// Starts listening to the client events.
|
||||||
|
void subscribeToEvents() {
|
||||||
|
if (_eventsSubscription != null) {
|
||||||
|
cancelEventSubscription();
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventsSubscription = CompositeSubscription();
|
||||||
|
_eventsSubscription!
|
||||||
|
..add(_client
|
||||||
.on()
|
.on()
|
||||||
.where((event) =>
|
.where((event) =>
|
||||||
event.me != null && event.type != EventType.healthCheck)
|
event.me != null && event.type != EventType.healthCheck)
|
||||||
.map((e) => e.me!)
|
.map((e) => e.me!)
|
||||||
.listen((user) => currentUser = currentUser?.merge(user) ?? user),
|
.listen((user) {
|
||||||
_client
|
currentUser = currentUser?.merge(user) ?? user;
|
||||||
|
}))
|
||||||
|
..add(_client
|
||||||
.on()
|
.on()
|
||||||
.map((event) => event.unreadChannels)
|
.map((event) => event.unreadChannels)
|
||||||
.whereType<int>()
|
.whereType<int>()
|
||||||
.listen((count) {
|
.listen((count) {
|
||||||
currentUser = currentUser?.copyWith(unreadChannels: count);
|
currentUser = currentUser?.copyWith(unreadChannels: count);
|
||||||
}),
|
}))
|
||||||
_client
|
..add(_client
|
||||||
.on()
|
.on()
|
||||||
.map((event) => event.totalUnreadCount)
|
.map((event) => event.totalUnreadCount)
|
||||||
.whereType<int>()
|
.whereType<int>()
|
||||||
.listen((count) {
|
.listen((count) {
|
||||||
currentUser = currentUser?.copyWith(totalUnreadCount: count);
|
currentUser = currentUser?.copyWith(totalUnreadCount: count);
|
||||||
}),
|
}));
|
||||||
]);
|
|
||||||
|
|
||||||
_listenChannelDeleted();
|
_listenChannelDeleted();
|
||||||
|
|
||||||
@@ -1485,56 +1525,73 @@ class ClientState {
|
|||||||
_listenAllChannelsRead();
|
_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
|
/// Pauses listening to the client events.
|
||||||
set totalUnreadCount(int unreadCount) {
|
void pauseEventSubscription([Future<void>? resumeSignal]) {
|
||||||
_totalUnreadCountController.add(unreadCount);
|
_eventsSubscription?.pause(resumeSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resumes listening to the client events.
|
||||||
|
void resumeEventSubscription() {
|
||||||
|
_eventsSubscription?.resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenChannelHidden() {
|
void _listenChannelHidden() {
|
||||||
_subscriptions
|
_eventsSubscription?.add(
|
||||||
.add(_client.on(EventType.channelHidden).listen((event) async {
|
_client.on(EventType.channelHidden).listen((event) async {
|
||||||
final eventChannel = event.channel!;
|
final eventChannel = event.channel!;
|
||||||
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
|
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
|
||||||
channels[eventChannel.cid]?.dispose();
|
channels[eventChannel.cid]?.dispose();
|
||||||
channels = channels..remove(eventChannel.cid);
|
channels = channels..remove(eventChannel.cid);
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenUserUpdated() {
|
void _listenUserUpdated() {
|
||||||
_subscriptions.add(_client.on(EventType.userUpdated).listen((event) {
|
_eventsSubscription?.add(
|
||||||
if (event.user!.id == currentUser!.id) {
|
_client.on(EventType.userUpdated).listen((event) {
|
||||||
currentUser = OwnUser.fromJson(event.user!.toJson());
|
if (event.user!.id == currentUser!.id) {
|
||||||
}
|
currentUser = OwnUser.fromJson(event.user!.toJson());
|
||||||
updateUser(event.user);
|
}
|
||||||
}));
|
updateUser(event.user);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenAllChannelsRead() {
|
void _listenAllChannelsRead() {
|
||||||
_subscriptions
|
_eventsSubscription?.add(
|
||||||
.add(_client.on(EventType.notificationMarkRead).listen((event) {
|
_client.on(EventType.notificationMarkRead).listen((event) {
|
||||||
if (event.cid == null) {
|
if (event.cid == null) {
|
||||||
channels.forEach((key, value) {
|
channels.forEach((key, value) {
|
||||||
value.state?.unreadCount = 0;
|
value.state?.unreadCount = 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenChannelDeleted() {
|
void _listenChannelDeleted() {
|
||||||
_subscriptions.add(_client
|
_eventsSubscription?.add(
|
||||||
.on(
|
_client
|
||||||
EventType.channelDeleted,
|
.on(
|
||||||
EventType.notificationRemovedFromChannel,
|
EventType.channelDeleted,
|
||||||
EventType.notificationChannelDeleted,
|
EventType.notificationRemovedFromChannel,
|
||||||
)
|
EventType.notificationChannelDeleted,
|
||||||
.listen((Event event) async {
|
)
|
||||||
final eventChannel = event.channel!;
|
.listen((Event event) async {
|
||||||
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
|
final eventChannel = event.channel!;
|
||||||
channels[eventChannel.cid]?.dispose();
|
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
|
||||||
channels = channels..remove(eventChannel.cid);
|
channels[eventChannel.cid]?.dispose();
|
||||||
}));
|
channels = channels..remove(eventChannel.cid);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final StreamChatClient _client;
|
final StreamChatClient _client;
|
||||||
@@ -1596,6 +1653,11 @@ class ClientState {
|
|||||||
_channelsController.add(newChannels);
|
_channelsController.add(newChannels);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Used internally for optimistic update of unread count
|
||||||
|
set totalUnreadCount(int unreadCount) {
|
||||||
|
_totalUnreadCountController.add(unreadCount);
|
||||||
|
}
|
||||||
|
|
||||||
void _computeUnreadCounts(OwnUser? user) {
|
void _computeUnreadCounts(OwnUser? user) {
|
||||||
final totalUnreadCount = user?.totalUnreadCount;
|
final totalUnreadCount = user?.totalUnreadCount;
|
||||||
if (totalUnreadCount != null) {
|
if (totalUnreadCount != null) {
|
||||||
@@ -1616,7 +1678,7 @@ class ClientState {
|
|||||||
|
|
||||||
/// Call this method to dispose this object
|
/// Call this method to dispose this object
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_subscriptions.forEach((s) => s.cancel());
|
cancelEventSubscription();
|
||||||
_currentUserController.close();
|
_currentUserController.close();
|
||||||
_unreadChannelsController.close();
|
_unreadChannelsController.close();
|
||||||
_totalUnreadCountController.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/http/stream_http_client.dart';
|
||||||
import 'package:stream_chat/src/core/models/attachment_file.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
|
/// Class responsible for uploading images and files to a given channel
|
||||||
abstract class AttachmentFileUploader {
|
abstract class AttachmentFileUploader {
|
||||||
/// Uploads a [image] to the given channel.
|
/// 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,
|
String channelType,
|
||||||
List<String> memberIds, {
|
List<String> memberIds, {
|
||||||
Message? message,
|
Message? message,
|
||||||
|
bool hideHistory = false,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await _client.post(
|
final response = await _client.post(
|
||||||
_getChannelUrl(channelId, channelType),
|
_getChannelUrl(channelId, channelType),
|
||||||
data: {
|
data: {
|
||||||
'add_members': memberIds,
|
'add_members': memberIds,
|
||||||
'message': message,
|
'message': message,
|
||||||
|
'hide_history': hideHistory,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return AddMembersResponse.fromJson(response.data);
|
return AddMembersResponse.fromJson(response.data);
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ class PaginationParams extends Equatable {
|
|||||||
/// ```
|
/// ```
|
||||||
const PaginationParams({
|
const PaginationParams({
|
||||||
this.limit = 10,
|
this.limit = 10,
|
||||||
this.before = 10,
|
|
||||||
this.after = 10,
|
|
||||||
this.offset,
|
this.offset,
|
||||||
this.next,
|
this.next,
|
||||||
this.idAround,
|
this.idAround,
|
||||||
@@ -88,14 +86,6 @@ class PaginationParams extends Equatable {
|
|||||||
/// The amount of items requested from the APIs.
|
/// The amount of items requested from the APIs.
|
||||||
final int limit;
|
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.
|
/// The offset of requesting items.
|
||||||
final int? offset;
|
final int? offset;
|
||||||
|
|
||||||
@@ -165,8 +155,6 @@ class PaginationParams extends Equatable {
|
|||||||
}) =>
|
}) =>
|
||||||
PaginationParams(
|
PaginationParams(
|
||||||
limit: limit ?? this.limit,
|
limit: limit ?? this.limit,
|
||||||
before: before ?? this.before,
|
|
||||||
after: limit ?? this.after,
|
|
||||||
offset: offset ?? this.offset,
|
offset: offset ?? this.offset,
|
||||||
idAround: idAround ?? this.idAround,
|
idAround: idAround ?? this.idAround,
|
||||||
next: next ?? this.next,
|
next: next ?? this.next,
|
||||||
@@ -186,8 +174,6 @@ class PaginationParams extends Equatable {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
limit,
|
limit,
|
||||||
before,
|
|
||||||
after,
|
|
||||||
offset,
|
offset,
|
||||||
next,
|
next,
|
||||||
idAround,
|
idAround,
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
|
|||||||
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
|
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
|
||||||
PaginationParams(
|
PaginationParams(
|
||||||
limit: json['limit'] as int? ?? 10,
|
limit: json['limit'] as int? ?? 10,
|
||||||
before: json['before'] as int? ?? 10,
|
|
||||||
after: json['after'] as int? ?? 10,
|
|
||||||
offset: json['offset'] as int?,
|
offset: json['offset'] as int?,
|
||||||
next: json['next'] as String?,
|
next: json['next'] as String?,
|
||||||
idAround: json['id_around'] as String?,
|
idAround: json['id_around'] as String?,
|
||||||
@@ -50,8 +48,6 @@ PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
|
|||||||
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
|
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
|
||||||
final val = <String, dynamic>{
|
final val = <String, dynamic>{
|
||||||
'limit': instance.limit,
|
'limit': instance.limit,
|
||||||
'before': instance.before,
|
|
||||||
'after': instance.after,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void writeNotNull(String key, dynamic value) {
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:stream_chat/src/client/client.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/error/error.dart';
|
||||||
import 'package:stream_chat/src/core/models/banned_user.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_model.dart';
|
||||||
import 'package:stream_chat/src/core/models/channel_state.dart';
|
import 'package:stream_chat/src/core/models/channel_state.dart';
|
||||||
import 'package:stream_chat/src/core/models/device.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) =>
|
static OGAttachmentResponse fromJson(Map<String, dynamic> json) =>
|
||||||
_$OGAttachmentResponseFromJson(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?
|
..title = json['title'] as String?
|
||||||
..titleLink = json['title_link'] as String?
|
..titleLink = json['title_link'] as String?
|
||||||
..type = json['type'] 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:logging/logging.dart';
|
||||||
import 'package:stream_chat/src/core/api/attachment_file_uploader.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/channel_api.dart';
|
||||||
import 'package:stream_chat/src/core/api/device_api.dart';
|
import 'package:stream_chat/src/core/api/device_api.dart';
|
||||||
import 'package:stream_chat/src/core/api/general_api.dart';
|
import 'package:stream_chat/src/core/api/general_api.dart';
|
||||||
@@ -22,9 +23,10 @@ class StreamChatApi {
|
|||||||
StreamHttpClientOptions? options,
|
StreamHttpClientOptions? options,
|
||||||
TokenManager? tokenManager,
|
TokenManager? tokenManager,
|
||||||
ConnectionIdManager? connectionIdManager,
|
ConnectionIdManager? connectionIdManager,
|
||||||
AttachmentFileUploader? attachmentFileUploader,
|
AttachmentFileUploaderProvider attachmentFileUploaderProvider =
|
||||||
|
StreamAttachmentFileUploader.new,
|
||||||
Logger? logger,
|
Logger? logger,
|
||||||
}) : _fileUploader = attachmentFileUploader,
|
}) : _fileUploaderProvider = attachmentFileUploaderProvider,
|
||||||
_client = client ??
|
_client = client ??
|
||||||
StreamHttpClient(
|
StreamHttpClient(
|
||||||
apiKey,
|
apiKey,
|
||||||
@@ -35,6 +37,7 @@ class StreamChatApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final StreamHttpClient _client;
|
final StreamHttpClient _client;
|
||||||
|
final AttachmentFileUploaderProvider _fileUploaderProvider;
|
||||||
|
|
||||||
UserApi? _user;
|
UserApi? _user;
|
||||||
|
|
||||||
@@ -51,6 +54,11 @@ class StreamChatApi {
|
|||||||
/// Api dedicated to message operations
|
/// Api dedicated to message operations
|
||||||
MessageApi get message => _message ??= MessageApi(_client);
|
MessageApi get message => _message ??= MessageApi(_client);
|
||||||
|
|
||||||
|
CallApi? _call;
|
||||||
|
|
||||||
|
/// Api dedicated to call operations
|
||||||
|
CallApi get call => _call ??= CallApi(_client);
|
||||||
|
|
||||||
ChannelApi? _channel;
|
ChannelApi? _channel;
|
||||||
|
|
||||||
/// Api dedicated to channel operations
|
/// Api dedicated to channel operations
|
||||||
@@ -75,5 +83,5 @@ class StreamChatApi {
|
|||||||
|
|
||||||
/// Class responsible for uploading images and files to a given channel
|
/// Class responsible for uploading images and files to a given channel
|
||||||
AttachmentFileUploader get fileUploader =>
|
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,
|
if (file?.mimeType != null) 'mime_type': file?.mimeType?.mimeType,
|
||||||
} {
|
} {
|
||||||
this.uploadState = uploadState ??
|
this.uploadState = uploadState ??
|
||||||
((assetUrl != null || imageUrl != null)
|
((assetUrl != null || imageUrl != null || thumbUrl != null)
|
||||||
? const UploadState.success()
|
? const UploadState.success()
|
||||||
: const UploadState.preparing());
|
: const UploadState.preparing());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,11 +47,12 @@ class AttachmentFile {
|
|||||||
final String? _name;
|
final String? _name;
|
||||||
|
|
||||||
/// File name including its extension.
|
/// 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
|
/// Byte data for this file. Particularly useful if you want to manipulate
|
||||||
/// its data or easily upload to somewhere else.
|
/// its data or easily upload to somewhere else.
|
||||||
@JsonKey(toJson: _toString, fromJson: _fromString)
|
@JsonKey(ignore: true)
|
||||||
final Uint8List? bytes;
|
final Uint8List? bytes;
|
||||||
|
|
||||||
/// The file size in bytes.
|
/// The file size in bytes.
|
||||||
@@ -85,6 +86,22 @@ class AttachmentFile {
|
|||||||
}
|
}
|
||||||
return multiPartFile;
|
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.
|
/// Union class to hold various [UploadState] of a attachment.
|
||||||
@@ -124,13 +141,3 @@ class UploadState with _$UploadState {
|
|||||||
/// Returns true if state is [Failed]
|
/// Returns true if state is [Failed]
|
||||||
bool get isFailed => this 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
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return _$$PreparingToJson(this);
|
return _$$PreparingToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +394,9 @@ class _$InProgress extends InProgress {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
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) =
|
factory InProgress.fromJson(Map<String, dynamic> json) =
|
||||||
_$InProgress.fromJson;
|
_$InProgress.fromJson;
|
||||||
|
|
||||||
int get uploaded => throw _privateConstructorUsedError;
|
int get uploaded;
|
||||||
int get total => throw _privateConstructorUsedError;
|
int get total;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
_$$InProgressCopyWith<_$InProgress> get copyWith =>
|
_$$InProgressCopyWith<_$InProgress> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@@ -531,7 +535,9 @@ class _$Success extends Success {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return _$$SuccessToJson(this);
|
return _$$SuccessToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,7 +692,9 @@ class _$Failed extends Failed {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
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;
|
factory Failed.fromJson(Map<String, dynamic> json) = _$Failed.fromJson;
|
||||||
|
|
||||||
String get error => throw _privateConstructorUsedError;
|
String get error;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
_$$FailedCopyWith<_$Failed> get copyWith =>
|
_$$FailedCopyWith<_$Failed> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ AttachmentFile _$AttachmentFileFromJson(Map<String, dynamic> json) =>
|
|||||||
size: json['size'] as int?,
|
size: json['size'] as int?,
|
||||||
path: json['path'] as String?,
|
path: json['path'] as String?,
|
||||||
name: json['name'] as String?,
|
name: json['name'] as String?,
|
||||||
bytes: _fromString(json['bytes'] as String?),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$AttachmentFileToJson(AttachmentFile instance) =>
|
Map<String, dynamic> _$AttachmentFileToJson(AttachmentFile instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'path': instance.path,
|
'path': instance.path,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'bytes': _toString(instance.bytes),
|
|
||||||
'size': instance.size,
|
'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(),
|
createdAt = createdAt ?? DateTime.now(),
|
||||||
updatedAt = updatedAt ?? DateTime.now(),
|
updatedAt = updatedAt ?? DateTime.now(),
|
||||||
|
|
||||||
// TODO: Make them top-level fields in v5
|
|
||||||
// For backwards compatibility, set 'disabled', 'hidden'
|
// For backwards compatibility, set 'disabled', 'hidden'
|
||||||
// and 'truncated_at' in [extraData].
|
// and 'truncated_at' in [extraData].
|
||||||
extraData = {
|
extraData = {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class Member extends Equatable {
|
|||||||
this.inviteAcceptedAt,
|
this.inviteAcceptedAt,
|
||||||
this.inviteRejectedAt,
|
this.inviteRejectedAt,
|
||||||
this.invited = false,
|
this.invited = false,
|
||||||
this.role,
|
|
||||||
this.channelRole,
|
this.channelRole,
|
||||||
this.userId,
|
this.userId,
|
||||||
this.isModerator = false,
|
this.isModerator = false,
|
||||||
@@ -48,10 +47,6 @@ class Member extends Equatable {
|
|||||||
/// True if the user has been invited to the channel
|
/// True if the user has been invited to the channel
|
||||||
final bool invited;
|
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
|
/// The role of this member in the channel
|
||||||
final String? channelRole;
|
final String? channelRole;
|
||||||
|
|
||||||
@@ -100,7 +95,6 @@ class Member extends Equatable {
|
|||||||
banned: banned ?? this.banned,
|
banned: banned ?? this.banned,
|
||||||
banExpires: banExpires ?? this.banExpires,
|
banExpires: banExpires ?? this.banExpires,
|
||||||
shadowBanned: shadowBanned ?? this.shadowBanned,
|
shadowBanned: shadowBanned ?? this.shadowBanned,
|
||||||
role: role ?? this.role,
|
|
||||||
channelRole: channelRole ?? this.channelRole,
|
channelRole: channelRole ?? this.channelRole,
|
||||||
userId: userId ?? this.userId,
|
userId: userId ?? this.userId,
|
||||||
isModerator: isModerator ?? this.isModerator,
|
isModerator: isModerator ?? this.isModerator,
|
||||||
@@ -117,7 +111,6 @@ class Member extends Equatable {
|
|||||||
inviteAcceptedAt,
|
inviteAcceptedAt,
|
||||||
inviteRejectedAt,
|
inviteRejectedAt,
|
||||||
invited,
|
invited,
|
||||||
role,
|
|
||||||
channelRole,
|
channelRole,
|
||||||
userId,
|
userId,
|
||||||
isModerator,
|
isModerator,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ Member _$MemberFromJson(Map<String, dynamic> json) => Member(
|
|||||||
? null
|
? null
|
||||||
: DateTime.parse(json['invite_rejected_at'] as String),
|
: DateTime.parse(json['invite_rejected_at'] as String),
|
||||||
invited: json['invited'] as bool? ?? false,
|
invited: json['invited'] as bool? ?? false,
|
||||||
role: json['role'] as String?,
|
|
||||||
channelRole: json['channel_role'] as String?,
|
channelRole: json['channel_role'] as String?,
|
||||||
userId: json['user_id'] as String?,
|
userId: json['user_id'] as String?,
|
||||||
isModerator: json['is_moderator'] as bool? ?? false,
|
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_accepted_at': instance.inviteAcceptedAt?.toIso8601String(),
|
||||||
'invite_rejected_at': instance.inviteRejectedAt?.toIso8601String(),
|
'invite_rejected_at': instance.inviteRejectedAt?.toIso8601String(),
|
||||||
'invited': instance.invited,
|
'invited': instance.invited,
|
||||||
'role': instance.role,
|
|
||||||
'channel_role': instance.channelRole,
|
'channel_role': instance.channelRole,
|
||||||
'user_id': instance.userId,
|
'user_id': instance.userId,
|
||||||
'is_moderator': instance.isModerator,
|
'is_moderator': instance.isModerator,
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ class User extends Equatable {
|
|||||||
this.language,
|
this.language,
|
||||||
}) : createdAt = createdAt ?? DateTime.now(),
|
}) : createdAt = createdAt ?? DateTime.now(),
|
||||||
updatedAt = updatedAt ?? DateTime.now(),
|
updatedAt = updatedAt ?? DateTime.now(),
|
||||||
// TODO: Make them top-level fields in v5
|
|
||||||
// For backwards compatibility, set 'name', 'image' in [extraData].
|
// For backwards compatibility, set 'name', 'image' in [extraData].
|
||||||
extraData = {
|
extraData = {
|
||||||
...extraData,
|
...extraData,
|
||||||
|
|||||||
@@ -4,25 +4,25 @@ import 'package:stream_chat/src/core/platform_detector/platform_detector_stub.da
|
|||||||
|
|
||||||
/// Possible platforms
|
/// Possible platforms
|
||||||
enum PlatformType {
|
enum PlatformType {
|
||||||
///
|
/// Android: <https://www.android.com/>
|
||||||
android,
|
android,
|
||||||
|
|
||||||
///
|
/// iOS: <https://www.apple.com/ios/>
|
||||||
ios,
|
ios,
|
||||||
|
|
||||||
///
|
/// web: <https://en.wikipedia.org/wiki/World_Wide_Web>
|
||||||
web,
|
web,
|
||||||
|
|
||||||
///
|
/// macOS: <https://www.apple.com/macos>
|
||||||
macOS,
|
macOS,
|
||||||
|
|
||||||
///
|
/// Windows: <https://www.windows.com>
|
||||||
windows,
|
windows,
|
||||||
|
|
||||||
///
|
/// Linux: <https://www.linux.org>
|
||||||
linux,
|
linux,
|
||||||
|
|
||||||
///
|
/// Fuchsia: <https://fuchsia.dev/fuchsia-src/concepts>
|
||||||
fuchsia,
|
fuchsia,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +51,9 @@ class CurrentPlatform {
|
|||||||
/// True if the app is running on fuchsia
|
/// True if the app is running on fuchsia
|
||||||
static bool get isFuchsia => type == PlatformType.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
|
/// Returns a string version of the platform
|
||||||
static String get name {
|
static String get name {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -68,8 +71,6 @@ class CurrentPlatform {
|
|||||||
return 'linux';
|
return 'linux';
|
||||||
case PlatformType.fuchsia:
|
case PlatformType.fuchsia:
|
||||||
return 'fuchsia';
|
return 'fuchsia';
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,3 +10,8 @@ PlatformType get currentPlatform {
|
|||||||
if (Platform.isIOS) return PlatformType.ios;
|
if (Platform.isIOS) return PlatformType.ios;
|
||||||
return PlatformType.android;
|
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';
|
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||||
|
|
||||||
/// Stub implementation
|
/// Stub implementation
|
||||||
PlatformType get currentPlatform {
|
PlatformType get currentPlatform => throw UnimplementedError();
|
||||||
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
|
/// Version running on web
|
||||||
PlatformType get currentPlatform => PlatformType.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/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_model.dart';
|
||||||
import 'package:stream_chat/src/core/models/channel_state.dart';
|
import 'package:stream_chat/src/core/models/channel_state.dart';
|
||||||
import 'package:stream_chat/src/core/models/event.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/reaction.dart';
|
||||||
import 'package:stream_chat/src/core/models/read.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/models/user.dart';
|
||||||
|
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||||
import 'package:stream_chat/src/core/util/extension.dart';
|
import 'package:stream_chat/src/core/util/extension.dart';
|
||||||
|
|
||||||
/// A simple client used for persisting chat data locally.
|
/// A simple client used for persisting chat data locally.
|
||||||
@@ -133,7 +135,7 @@ abstract class ChatPersistenceClient {
|
|||||||
/// Remove a pinned message by message [cids]
|
/// Remove a pinned message by message [cids]
|
||||||
Future<void> deletePinnedMessageByCids(List<String> cids);
|
Future<void> deletePinnedMessageByCids(List<String> cids);
|
||||||
|
|
||||||
/// Remove a channel by [cid]
|
/// Remove a channel by [channelId]
|
||||||
Future<void> deleteChannels(List<String> cids);
|
Future<void> deleteChannels(List<String> cids);
|
||||||
|
|
||||||
/// Updates the message data of a particular channel [cid] with
|
/// Updates the message data of a particular channel [cid] with
|
||||||
@@ -243,7 +245,15 @@ abstract class ChatPersistenceClient {
|
|||||||
final cid = channel.cid;
|
final cid = channel.cid;
|
||||||
final reads = state.read;
|
final reads = state.read;
|
||||||
final members = state.members;
|
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;
|
final pinnedMessages = state.pinnedMessages;
|
||||||
|
|
||||||
// Preparing deletion data
|
// Preparing deletion data
|
||||||
@@ -255,7 +265,7 @@ abstract class ChatPersistenceClient {
|
|||||||
// preparing addition data
|
// preparing addition data
|
||||||
channelWithReads[cid] = reads;
|
channelWithReads[cid] = reads;
|
||||||
channelWithMembers[cid] = members;
|
channelWithMembers[cid] = members;
|
||||||
channelWithMessages[cid] = messages;
|
channelWithMessages[cid] = messages?.toList();
|
||||||
channelWithPinnedMessages[cid] = pinnedMessages;
|
channelWithPinnedMessages[cid] = pinnedMessages;
|
||||||
|
|
||||||
reactions.addAll(messages?.expand(_expandReactions) ?? []);
|
reactions.addAll(messages?.expand(_expandReactions) ?? []);
|
||||||
|
|||||||
@@ -436,6 +436,9 @@ class WebSocket with TimerHelper {
|
|||||||
/// Disconnects the WS and releases eventual resources
|
/// Disconnects the WS and releases eventual resources
|
||||||
void disconnect() {
|
void disconnect() {
|
||||||
if (connectionStatus == ConnectionStatus.disconnected) return;
|
if (connectionStatus == ConnectionStatus.disconnected) return;
|
||||||
|
|
||||||
|
_resetRequestFlags(resetAttempts: true);
|
||||||
|
|
||||||
_connectionStatus = ConnectionStatus.disconnected;
|
_connectionStatus = ConnectionStatus.disconnected;
|
||||||
|
|
||||||
_logger?.info('Disconnecting web-socket connection');
|
_logger?.info('Disconnecting web-socket connection');
|
||||||
@@ -447,6 +450,7 @@ class WebSocket with TimerHelper {
|
|||||||
_stopMonitoringEvents();
|
_stopMonitoringEvents();
|
||||||
|
|
||||||
_manuallyClosed = true;
|
_manuallyClosed = true;
|
||||||
|
|
||||||
_closeWebSocketChannel();
|
_closeWebSocketChannel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
library stream_chat;
|
library stream_chat;
|
||||||
|
|
||||||
export 'package:async/async.dart';
|
export 'package:async/async.dart';
|
||||||
|
export 'package:dio/src/cancel_token.dart';
|
||||||
export 'package:dio/src/dio_error.dart';
|
export 'package:dio/src/dio_error.dart';
|
||||||
export 'package:dio/src/multipart_file.dart';
|
export 'package:dio/src/multipart_file.dart';
|
||||||
export 'package:dio/src/options.dart';
|
export 'package:dio/src/options.dart';
|
||||||
@@ -9,8 +10,7 @@ export 'package:logging/logging.dart' show Logger, Level, LogRecord;
|
|||||||
export 'package:rate_limiter/rate_limiter.dart';
|
export 'package:rate_limiter/rate_limiter.dart';
|
||||||
export 'package:uuid/uuid.dart';
|
export 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
export './src/core/api/attachment_file_uploader.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/requests.dart';
|
export './src/core/api/requests.dart';
|
||||||
export './src/core/api/responses.dart';
|
export './src/core/api/responses.dart';
|
||||||
@@ -40,11 +40,12 @@ export './src/permission_type.dart';
|
|||||||
export './src/ws/connection_status.dart';
|
export './src/ws/connection_status.dart';
|
||||||
export 'src/client/channel.dart';
|
export 'src/client/channel.dart';
|
||||||
export 'src/client/client.dart';
|
export 'src/client/client.dart';
|
||||||
|
export 'src/client/key_stroke_handler.dart';
|
||||||
export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader;
|
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/requests.dart';
|
export 'src/core/api/requests.dart';
|
||||||
export 'src/core/api/responses.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/error/error.dart';
|
||||||
export 'src/core/models/action.dart';
|
export 'src/core/models/action.dart';
|
||||||
export 'src/core/models/attachment.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/reaction.dart';
|
||||||
export 'src/core/models/read.dart';
|
export 'src/core/models/read.dart';
|
||||||
export 'src/core/models/user.dart';
|
export 'src/core/models/user.dart';
|
||||||
|
export 'src/core/platform_detector/platform_detector.dart';
|
||||||
export 'src/core/util/extension.dart';
|
export 'src/core/util/extension.dart';
|
||||||
export 'src/db/chat_persistence_client.dart';
|
export 'src/db/chat_persistence_client.dart';
|
||||||
export 'src/event_type.dart';
|
export 'src/event_type.dart';
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
|
|||||||
/// Current package version
|
/// Current package version
|
||||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||||
// ignore: constant_identifier_names
|
// ignore: constant_identifier_names
|
||||||
const PACKAGE_VERSION = '4.3.0';
|
const PACKAGE_VERSION = '5.0.0';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat
|
name: stream_chat
|
||||||
homepage: https://getstream.io/
|
homepage: https://getstream.io/
|
||||||
description: The official Dart client for Stream Chat, a service for building chat applications.
|
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
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
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', () {
|
group('Initialized Channel with Persistence', () {
|
||||||
late final client = MockStreamChatClientWithPersistence();
|
late final client = MockStreamChatClientWithPersistence();
|
||||||
const channelId = 'test-channel-id';
|
const channelId = 'test-channel-id';
|
||||||
@@ -1832,7 +1831,10 @@ void main() {
|
|||||||
..message = updateMessage,
|
..message = updateMessage,
|
||||||
);
|
);
|
||||||
|
|
||||||
final res = await channel.update(channelData, updateMessage);
|
final res = await channel.update(
|
||||||
|
channelData,
|
||||||
|
updateMessage: updateMessage,
|
||||||
|
);
|
||||||
|
|
||||||
expect(res, isNotNull);
|
expect(res, isNotNull);
|
||||||
expect(res.channel.cid, channelModel.cid);
|
expect(res.channel.cid, channelModel.cid);
|
||||||
@@ -2028,7 +2030,7 @@ void main() {
|
|||||||
..message = message,
|
..message = message,
|
||||||
);
|
);
|
||||||
|
|
||||||
final res = await channel.addMembers(memberIds, message);
|
final res = await channel.addMembers(memberIds, message: message);
|
||||||
|
|
||||||
expect(res, isNotNull);
|
expect(res, isNotNull);
|
||||||
expect(res.channel.cid, channelModel.cid);
|
expect(res.channel.cid, channelModel.cid);
|
||||||
@@ -2060,7 +2062,7 @@ void main() {
|
|||||||
..message = message,
|
..message = message,
|
||||||
);
|
);
|
||||||
|
|
||||||
final res = await channel.inviteMembers(memberIds, message);
|
final res = await channel.inviteMembers(memberIds, message: message);
|
||||||
|
|
||||||
expect(res, isNotNull);
|
expect(res, isNotNull);
|
||||||
expect(res.channel.cid, channelModel.cid);
|
expect(res.channel.cid, channelModel.cid);
|
||||||
@@ -2093,7 +2095,7 @@ void main() {
|
|||||||
..message = message,
|
..message = message,
|
||||||
);
|
);
|
||||||
|
|
||||||
final res = await channel.removeMembers(memberIds, message);
|
final res = await channel.removeMembers(memberIds, message: message);
|
||||||
|
|
||||||
expect(res, isNotNull);
|
expect(res, isNotNull);
|
||||||
expect(res.channel.cid, channelModel.cid);
|
expect(res.channel.cid, channelModel.cid);
|
||||||
@@ -2658,16 +2660,22 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test(
|
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 {
|
() 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(() => channel.config?.typingEvents).thenReturn(true);
|
||||||
|
|
||||||
when(() => client.sendEvent(
|
when(() => client.sendEvent(
|
||||||
channelId,
|
channelId,
|
||||||
channelType,
|
channelType,
|
||||||
any(that: isSameEventAs(typingEvent)),
|
any(that: isSameEventAs(startTypingEvent)),
|
||||||
|
)).thenAnswer((_) async => EmptyResponse());
|
||||||
|
when(() => client.sendEvent(
|
||||||
|
channelId,
|
||||||
|
channelType,
|
||||||
|
any(that: isSameEventAs(stopTypingEvent)),
|
||||||
)).thenAnswer((_) async => EmptyResponse());
|
)).thenAnswer((_) async => EmptyResponse());
|
||||||
|
|
||||||
await channel.keyStroke();
|
await channel.keyStroke();
|
||||||
@@ -2675,7 +2683,12 @@ void main() {
|
|||||||
verify(() => client.sendEvent(
|
verify(() => client.sendEvent(
|
||||||
channelId,
|
channelId,
|
||||||
channelType,
|
channelType,
|
||||||
any(that: isSameEventAs(typingEvent)),
|
any(that: isSameEventAs(startTypingEvent)),
|
||||||
|
)).called(1);
|
||||||
|
verify(() => client.sendEvent(
|
||||||
|
channelId,
|
||||||
|
channelType,
|
||||||
|
any(that: isSameEventAs(stopTypingEvent)),
|
||||||
)).called(1);
|
)).called(1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -2688,7 +2701,7 @@ void main() {
|
|||||||
|
|
||||||
final typingStopEvent = Event(type: EventType.typingStop);
|
final typingStopEvent = Event(type: EventType.typingStop);
|
||||||
|
|
||||||
await channel.keyStroke();
|
await channel.stopTyping();
|
||||||
|
|
||||||
verifyNever(() => client.sendEvent(
|
verifyNever(() => client.sendEvent(
|
||||||
channelId,
|
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);
|
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'];
|
const memberIds = ['test-member-id-1', 'test-member-id-2'];
|
||||||
final channelModel = ChannelModel(id: channelId, type: channelType);
|
final channelModel = ChannelModel(id: channelId, type: channelType);
|
||||||
final message = Message(id: 'test-message-id', text: 'members-added');
|
final message = Message(id: 'test-message-id', text: 'members-added');
|
||||||
|
const hideHistory = true;
|
||||||
|
|
||||||
final path = _getChannelUrl(channelId, channelType);
|
final path = _getChannelUrl(channelId, channelType);
|
||||||
|
|
||||||
@@ -384,6 +385,7 @@ void main() {
|
|||||||
data: {
|
data: {
|
||||||
'add_members': memberIds,
|
'add_members': memberIds,
|
||||||
'message': message,
|
'message': message,
|
||||||
|
'hide_history': hideHistory,
|
||||||
},
|
},
|
||||||
)).thenAnswer((_) async => successResponse(path, data: {
|
)).thenAnswer((_) async => successResponse(path, data: {
|
||||||
'channel': channelModel.toJson(),
|
'channel': channelModel.toJson(),
|
||||||
@@ -395,6 +397,7 @@ void main() {
|
|||||||
channelType,
|
channelType,
|
||||||
memberIds,
|
memberIds,
|
||||||
message: message,
|
message: message,
|
||||||
|
hideHistory: hideHistory,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(res, isNotNull);
|
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 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:stream_chat/src/core/models/call_payload.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
@@ -4340,5 +4341,31 @@ void main() {
|
|||||||
expect(response.members, isA<List<Member>>());
|
expect(response.members, isA<List<Member>>());
|
||||||
expect(response.message, isA<Message>());
|
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>());
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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/guest_api.dart';
|
||||||
import 'package:stream_chat/src/core/api/message_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/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/api/user_api.dart';
|
||||||
import 'package:stream_chat/src/core/http/token.dart';
|
import 'package:stream_chat/src/core/http/token.dart';
|
||||||
import 'package:stream_chat/src/core/http/token_manager.dart';
|
import 'package:stream_chat/src/core/http/token_manager.dart';
|
||||||
|
|||||||
@@ -131,6 +131,40 @@ void main() {
|
|||||||
addTearDown(timer.cancel);
|
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 {
|
test('`connect` should throw if already in connection attempt', () async {
|
||||||
final user = OwnUser(id: 'test-user');
|
final user = OwnUser(id: 'test-user');
|
||||||
webSocket.connect(user);
|
webSocket.connect(user);
|
||||||
|
|||||||
@@ -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
|
## 4.3.0
|
||||||
|
|
||||||
- Updated `photo_view` dependency to [`0.14.0`](https://pub.dev/packages/photo_view/changelog).
|
- 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
|
- [[#996]](https://github.com/GetStream/stream-chat-flutter/issues/996) Videos break bottom photo
|
||||||
carousal.
|
carousal.
|
||||||
- Fix: URLs with path and/or query params are not enriched.
|
- 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
|
✅ Added
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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.
|
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
|
### Troubleshooting
|
||||||
|
|
||||||
It may happen that you have some problems building the app.
|
It may happen that you have some problems building the app.
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
/build/
|
/build/
|
||||||
|
|
||||||
# Web related
|
# Web related
|
||||||
lib/generated_plugin_registrant.dart
|
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "com.example.example"
|
applicationId "com.example.example"
|
||||||
minSdkVersion 22
|
minSdkVersion 23
|
||||||
targetSdkVersion 31
|
targetSdkVersion 31
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
additional functionality it is fine to subclass or reimplement
|
additional functionality it is fine to subclass or reimplement
|
||||||
FlutterApplication and put your custom class here. -->
|
FlutterApplication and put your custom class here. -->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<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
|
<application
|
||||||
android:name=".Application"
|
android:name=".Application"
|
||||||
android:label="example"
|
android:label="example"
|
||||||
|
|||||||
@@ -21,6 +21,6 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>9.0</string>
|
<string>11.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -339,7 +339,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@@ -425,7 +425,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -474,7 +474,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
|
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
|
||||||
|
|
||||||
void main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
/// Create a new instance of [StreamChatClient] passing the apikey obtained
|
/// Create a new instance of [StreamChatClient] passing the apikey obtained
|
||||||
/// from your project dashboard.
|
/// from your project dashboard.
|
||||||
final client = StreamChatClient(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.OFF,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Set the current user and connect the websocket. In a production
|
/// Set the current user and connect the websocket. In a production
|
||||||
@@ -63,50 +66,272 @@ class MyApp extends StatelessWidget {
|
|||||||
final Channel channel;
|
final Channel channel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => MaterialApp(
|
Widget build(BuildContext context) {
|
||||||
theme: ThemeData.light(),
|
return MaterialApp(
|
||||||
darkTheme: ThemeData.dark(),
|
debugShowCheckedModeBanner: false,
|
||||||
supportedLocales: const [
|
theme: ThemeData.light(),
|
||||||
Locale('en'),
|
darkTheme: ThemeData.dark(),
|
||||||
Locale('hi'),
|
// themeMode: ThemeMode.dark,
|
||||||
Locale('fr'),
|
supportedLocales: const [
|
||||||
Locale('it'),
|
Locale('en'),
|
||||||
Locale('es'),
|
Locale('hi'),
|
||||||
],
|
Locale('fr'),
|
||||||
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
|
Locale('it'),
|
||||||
builder: (context, widget) => StreamChat(
|
Locale('es'),
|
||||||
client: client,
|
],
|
||||||
child: widget,
|
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
|
||||||
),
|
builder: (context, widget) => StreamChat(
|
||||||
home: StreamChannel(
|
client: client,
|
||||||
channel: channel,
|
child: widget,
|
||||||
child: const ChannelPage(),
|
),
|
||||||
),
|
home: StreamChannel(
|
||||||
);
|
channel: channel,
|
||||||
|
child: const ResponsiveChat(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A list of messages sent in the current channel.
|
class ResponsiveChat extends StatelessWidget {
|
||||||
///
|
const ResponsiveChat({
|
||||||
/// 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({
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) {
|
||||||
appBar: const StreamChannelHeader(),
|
return ResponsiveBuilder(
|
||||||
body: Column(
|
builder: (context, sizingInformation) {
|
||||||
children: const <Widget>[
|
if (sizingInformation.isMobile) {
|
||||||
Expanded(
|
return ChannelListPage(
|
||||||
child: StreamMessageListView(),
|
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:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
void main() async {
|
Future<void> main() async {
|
||||||
final client = StreamChatClient(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
@@ -29,13 +29,15 @@ class MyApp extends StatelessWidget {
|
|||||||
final StreamChatClient client;
|
final StreamChatClient client;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => MaterialApp(
|
Widget build(BuildContext context) {
|
||||||
builder: (context, child) => StreamChat(
|
return MaterialApp(
|
||||||
client: client,
|
builder: (context, child) => StreamChat(
|
||||||
child: child,
|
client: client,
|
||||||
),
|
child: child,
|
||||||
home: const SplitView(),
|
),
|
||||||
);
|
home: const SplitView(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SplitView extends StatefulWidget {
|
class SplitView extends StatefulWidget {
|
||||||
@@ -51,37 +53,39 @@ class _SplitViewState extends State<SplitView> {
|
|||||||
Channel? selectedChannel;
|
Channel? selectedChannel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Flex(
|
Widget build(BuildContext context) {
|
||||||
direction: Axis.horizontal,
|
return Flex(
|
||||||
children: <Widget>[
|
direction: Axis.horizontal,
|
||||||
Flexible(
|
children: <Widget>[
|
||||||
child: ChannelListPage(
|
Flexible(
|
||||||
onTap: (channel) {
|
child: ChannelListPage(
|
||||||
setState(() {
|
onTap: (channel) {
|
||||||
selectedChannel = channel;
|
setState(() {
|
||||||
});
|
selectedChannel = channel;
|
||||||
},
|
});
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
Flexible(
|
),
|
||||||
flex: 2,
|
Flexible(
|
||||||
child: Scaffold(
|
flex: 2,
|
||||||
body: selectedChannel != null
|
child: Scaffold(
|
||||||
? StreamChannel(
|
body: selectedChannel != null
|
||||||
key: ValueKey(selectedChannel!.cid),
|
? StreamChannel(
|
||||||
channel: selectedChannel!,
|
key: ValueKey(selectedChannel!.cid),
|
||||||
child: const ChannelPage(),
|
channel: selectedChannel!,
|
||||||
)
|
child: const ChannelPage(),
|
||||||
: Center(
|
)
|
||||||
child: Text(
|
: Center(
|
||||||
'Pick a channel to show the messages 💬',
|
child: Text(
|
||||||
style: Theme.of(context).textTheme.headline5,
|
'Pick a channel to show the messages 💬',
|
||||||
),
|
style: Theme.of(context).textTheme.headline5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChannelListPage extends StatefulWidget {
|
class ChannelListPage extends StatefulWidget {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
// ignore_for_file: public_member_api_docs
|
||||||
// ignore_for_file: prefer_expression_function_bodies
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.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]
|
/// and [StreamMessageInput]
|
||||||
///
|
///
|
||||||
/// If you now run the simulator you will see a single channel UI.
|
/// If you now run the simulator you will see a single channel UI.
|
||||||
void main() async {
|
Future<void> main() async {
|
||||||
final client = StreamChatClient(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
@@ -90,15 +89,18 @@ class ChannelPage extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
// ignore: prefer_expression_function_bodies
|
||||||
appBar: const StreamChannelHeader(),
|
Widget build(BuildContext context) {
|
||||||
body: Column(
|
return Scaffold(
|
||||||
children: const <Widget>[
|
appBar: const StreamChannelHeader(),
|
||||||
Expanded(
|
body: Column(
|
||||||
child: StreamMessageListView(),
|
children: const <Widget>[
|
||||||
),
|
Expanded(
|
||||||
StreamMessageInput(),
|
child: StreamMessageListView(),
|
||||||
],
|
),
|
||||||
),
|
StreamMessageInput(),
|
||||||
);
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
// ignore_for_file: public_member_api_docs
|
||||||
// ignore_for_file: prefer_expression_function_bodies
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.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
|
/// [StreamChannelListView] handles pagination
|
||||||
/// and updates automatically when new channels are created or when a new
|
/// and updates automatically when new channels are created or when a new
|
||||||
/// message is added to a channel.
|
/// message is added to a channel.
|
||||||
void main() async {
|
Future<void> main() async {
|
||||||
final client = StreamChatClient(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
@@ -100,23 +98,25 @@ class _ChannelListPageState extends State<ChannelListPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) {
|
||||||
body: RefreshIndicator(
|
return Scaffold(
|
||||||
onRefresh: _controller.refresh,
|
body: RefreshIndicator(
|
||||||
child: StreamChannelListView(
|
onRefresh: _controller.refresh,
|
||||||
controller: _controller,
|
child: StreamChannelListView(
|
||||||
onChannelTap: (channel) => Navigator.push(
|
controller: _controller,
|
||||||
context,
|
onChannelTap: (channel) => Navigator.push(
|
||||||
MaterialPageRoute(
|
context,
|
||||||
builder: (_) => StreamChannel(
|
MaterialPageRoute(
|
||||||
channel: channel,
|
builder: (_) => StreamChannel(
|
||||||
child: const ChannelPage(),
|
channel: channel,
|
||||||
),
|
child: const ChannelPage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChannelPage extends StatelessWidget {
|
class ChannelPage extends StatelessWidget {
|
||||||
@@ -125,15 +125,17 @@ class ChannelPage extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) {
|
||||||
appBar: const StreamChannelHeader(),
|
return Scaffold(
|
||||||
body: Column(
|
appBar: const StreamChannelHeader(),
|
||||||
children: const <Widget>[
|
body: Column(
|
||||||
Expanded(
|
children: const <Widget>[
|
||||||
child: StreamMessageListView(),
|
Expanded(
|
||||||
),
|
child: StreamMessageListView(),
|
||||||
StreamMessageInput(),
|
),
|
||||||
],
|
StreamMessageInput(),
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
// ignore_for_file: public_member_api_docs
|
||||||
// ignore_for_file: prefer_expression_function_bodies
|
|
||||||
import 'package:collection/collection.dart' show IterableExtension;
|
import 'package:collection/collection.dart' show IterableExtension;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
// ignore_for_file: public_member_api_docs
|
||||||
// ignore_for_file: prefer_expression_function_bodies
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
@@ -106,21 +104,23 @@ class ChannelPage extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) {
|
||||||
appBar: const StreamChannelHeader(),
|
return Scaffold(
|
||||||
body: Column(
|
appBar: const StreamChannelHeader(),
|
||||||
children: <Widget>[
|
body: Column(
|
||||||
Expanded(
|
children: <Widget>[
|
||||||
child: StreamMessageListView(
|
Expanded(
|
||||||
threadBuilder: (_, parentMessage) => ThreadPage(
|
child: StreamMessageListView(
|
||||||
parent: parentMessage,
|
threadBuilder: (_, parentMessage) => ThreadPage(
|
||||||
),
|
parent: parentMessage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const StreamMessageInput(),
|
),
|
||||||
],
|
const StreamMessageInput(),
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThreadPage extends StatelessWidget {
|
class ThreadPage extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
// ignore_for_file: public_member_api_docs
|
||||||
// ignore_for_file: prefer_expression_function_bodies
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
@@ -86,21 +85,23 @@ class _ChannelListPageState extends State<ChannelListPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) {
|
||||||
body: StreamChannelListView(
|
return Scaffold(
|
||||||
controller: _listController,
|
body: StreamChannelListView(
|
||||||
onChannelTap: (channel) {
|
controller: _listController,
|
||||||
Navigator.of(context).push(
|
onChannelTap: (channel) {
|
||||||
MaterialPageRoute(
|
Navigator.of(context).push(
|
||||||
builder: (context) => StreamChannel(
|
MaterialPageRoute(
|
||||||
channel: channel,
|
builder: (context) => StreamChannel(
|
||||||
child: const ChannelPage(),
|
channel: channel,
|
||||||
),
|
child: const ChannelPage(),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChannelPage extends StatelessWidget {
|
class ChannelPage extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
// ignore_for_file: public_member_api_docs
|
||||||
// ignore_for_file: prefer_expression_function_bodies
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
@@ -126,21 +124,23 @@ class _ChannelListPageState extends State<ChannelListPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) {
|
||||||
body: StreamChannelListView(
|
return Scaffold(
|
||||||
controller: _listController,
|
body: StreamChannelListView(
|
||||||
onChannelTap: (channel) {
|
controller: _listController,
|
||||||
Navigator.of(context).push(
|
onChannelTap: (channel) {
|
||||||
MaterialPageRoute(
|
Navigator.of(context).push(
|
||||||
builder: (context) => StreamChannel(
|
MaterialPageRoute(
|
||||||
channel: channel,
|
builder: (context) => StreamChannel(
|
||||||
child: const ChannelPage(),
|
channel: channel,
|
||||||
),
|
child: const ChannelPage(),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChannelPage extends StatelessWidget {
|
class ChannelPage extends StatelessWidget {
|
||||||
@@ -149,21 +149,23 @@ class ChannelPage extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) {
|
||||||
appBar: const StreamChannelHeader(),
|
return Scaffold(
|
||||||
body: Column(
|
appBar: const StreamChannelHeader(),
|
||||||
children: <Widget>[
|
body: Column(
|
||||||
Expanded(
|
children: <Widget>[
|
||||||
child: StreamMessageListView(
|
Expanded(
|
||||||
threadBuilder: (_, parentMessage) => ThreadPage(
|
child: StreamMessageListView(
|
||||||
parent: parentMessage,
|
threadBuilder: (_, parentMessage) => ThreadPage(
|
||||||
),
|
parent: parentMessage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const StreamMessageInput(),
|
),
|
||||||
],
|
const StreamMessageInput(),
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThreadPage extends StatelessWidget {
|
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/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -28,5 +28,7 @@
|
|||||||
<string>MainMenu</string>
|
<string>MainMenu</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>In order to access your photo library</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import FlutterMacOS
|
|||||||
class MainFlutterWindow: NSWindow {
|
class MainFlutterWindow: NSWindow {
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
let flutterViewController = FlutterViewController.init()
|
let flutterViewController = FlutterViewController.init()
|
||||||
|
|
||||||
let windowFrame = self.frame
|
let windowFrame = self.frame
|
||||||
self.contentViewController = flutterViewController
|
self.contentViewController = flutterViewController
|
||||||
self.setFrame(windowFrame, display: true)
|
self.setFrame(windowFrame, display: true)
|
||||||
|
|
||||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||||
|
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,9 +24,10 @@ dependencies:
|
|||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
cupertino_icons: ^1.0.3
|
cupertino_icons: ^1.0.4
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
responsive_builder: ^0.4.2
|
||||||
stream_chat_flutter:
|
stream_chat_flutter:
|
||||||
path: ../
|
path: ../
|
||||||
stream_chat_localizations:
|
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:
|
Fore more details:
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||||
-->
|
-->
|
||||||
<base href="/">
|
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
@@ -31,7 +30,7 @@
|
|||||||
<title>example</title>
|
<title>example</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body oncontextmenu="return false;">
|
||||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||||
application. For more information, see:
|
application. For more information, see:
|
||||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
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
Reference in New Issue
Block a user