Release/1.4.0 beta (#318)
* fix: fixed long system messages overflowing * fix: fixed long system messages overflowing * [UI-Kit] Refactor, add support for downloading attachments. Signed-off-by: Sahil Kumar <[email protected]> * fix: header casing * [UI-Kit] Disable attachment download dialog pop using back button Signed-off-by: Sahil Kumar <[email protected]> * stream_chat_flutter fix svg on web * [UI-Kit] Pop attachment download dialog in case of any error. Signed-off-by: Sahil Kumar <[email protected]> * restore svg * feat: add pr title linter * fix test * fix client headers * update system message design * update gh token env var * add scopes * expose systemMessageBuilder in messagelistview * fix(ui): fix button size * add dispatch workflow * add dispatch workflow * update dispatch acton * add runson * fix ref * fix ref * update secret * specify repo * fix: list scroll keyboard behaviour (#304) * fix: Keyboard now closes by clicking on space or scrolling. * fix: Exposed system and normal message tap builders * fmt: dartfmt * feat: Add support for messages filter in `MessageListView` and `MessageListCore` (#303) * [MessageListView, MessageListCore] Add support for message filter Signed-off-by: Sahil Kumar <[email protected]> * [UI-Kit -> Pubspec] Remove `stream_chat_flutter_core` relative import Signed-off-by: Sahil Kumar <[email protected]> * add relative import * add relative import * remove svg for web * remove badge logic on web * style: Update for team lint (#297) * Update for team lint * fix tests * remove pedantic and sort deps * remove jiffy from system message Co-authored-by: Salvatore Giordano <[email protected]> * add section highlighting sample app repo (#307) Co-authored-by: Salvatore Giordano <[email protected]> * add null check to debounce * fix messagelistview loading errors * remove core relative import * fix: fixes message newline issue (#308) * fix: minor * fix: generate image thumbnails and use transparent image as placeholder for cached network images (#310) * update cdn handling * fix image attachment with new cdn * fix(ui): Fix message input permission request crash (#311) Signed-off-by: Sahil Kumar <[email protected]> * fix(llc): update member presence and add skip_push to message (#314) * update member presence * add skip_push to message * fix tests * fix: Fixed shimmer overflow (#316) * fix dialogs * fix(llc): Fix attachment upload state uneven progress. (#315) * feat(llc): Add rate limiter functions. fix(llc): Fix attachment upload state uneven progress. Signed-off-by: Sahil Kumar <[email protected]> * feat(llc): Export `async.dart` and `rate_limit.dart` Signed-off-by: Sahil Kumar <[email protected]> * refactor(llc): Replace our `rate_limit` implementation with the dart port of js `lodash` to better handlw `cancel` and `flush` functions. Signed-off-by: Sahil Kumar <[email protected]> * fix(UI-Kit): Make android example run on real devices Signed-off-by: Sahil Kumar <[email protected]> * fix offline behaviour Co-authored-by: Salvatore Giordano <[email protected]> * feat: Introduce onAttachmentTap on messages attachment (#309) * introdce onAttachmentTap on message * correct indent * sdfsd Co-authored-by: Salvatore Giordano <[email protected]> * feat: added customization options in main widgets (#312) * polish channelpreview customization options * polish channelheader customization options * polish messageinput customization options * polish threadheader customization options * polish channellistheader customization options * show the parent message if no messages in thread * fix edit message * fix channelinfo textstyle * fix review * fix useravatar * add ontitle tap to thread header * add custom message actions * add borderradius and button builder * add ontap to messageinput custom send button * fix bottom sheet * add doc * use placeholder image * add doc * fix listtile density * remove subtitle from ChannelListHeaderTheme.copyWith * add InputDecoration.merge extension * fix: use shimmer in images * fix: remove notification badge logic from the sdk and move it to sample app * fix import * fix: Method being called during build (#317) * method call bug fix * dartfmt * fix: show error messages as system and keep them in the message input (#319) * fix: pre release (#324) * use share_plus plugin * bump dependency versions * fix ui for web * unfocus messageinput only on commands * fix default error for messagesearchlistview * fix thumbnail animation * fix lint * ignore example in linter * update changelogs * fix lint * fix lint * update stream_chat pana min * add doc Co-authored-by: Sahil Kumar <[email protected]> Co-authored-by: Deven Joshi <[email protected]> Co-authored-by: Neevash Ramdial (Nash) <[email protected]> Co-authored-by: Nelson Nunes <[email protected]>
This commit is contained in:
co-authored by
Sahil Kumar
Deven Joshi
Neevash Ramdial
Nelson Nunes
parent
3332a48a0e
commit
4488fa6c3a
@@ -0,0 +1,30 @@
|
||||
name: dispatch_workflows
|
||||
|
||||
env:
|
||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
|
||||
jobs:
|
||||
dispatch_nightly:
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: benc-uk/workflow-dispatch@v1
|
||||
with:
|
||||
workflow: build_nightly
|
||||
repo: GetStream/flutter-samples
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
dispatch_stable:
|
||||
if: github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: benc-uk/workflow-dispatch@v1
|
||||
with:
|
||||
repo: GetStream/flutter-samples
|
||||
workflow: build
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
TOTAL_MAX: ${{ steps.analysis.outputs.total_max }}
|
||||
run: |
|
||||
PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX ))
|
||||
if (( $PERCENTAGE < 100 ))
|
||||
if (( $PERCENTAGE < 90 ))
|
||||
then
|
||||
echo Score too low!
|
||||
exit 1
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: 'PR Title is Conventional'
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/[email protected]
|
||||
with:
|
||||
scopes: |
|
||||
llc
|
||||
persistence
|
||||
core
|
||||
ui
|
||||
requireScope: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
run: melos bootstrap
|
||||
- name: 'Dart Analyze'
|
||||
run: |
|
||||
melos exec -c 3 -- \
|
||||
melos exec -c 3 --ignore="*example*" -- \
|
||||
tuneup check
|
||||
- name: 'Pub Check'
|
||||
run: |
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ build/
|
||||
.project
|
||||
.classpath
|
||||
.settings
|
||||
/.fvm
|
||||
**/.fvm
|
||||
|
||||
.melos_tool/
|
||||
/packages/flutter_widgets/example/ios/Flutter/.last_build_id
|
||||
|
||||
@@ -17,6 +17,9 @@ This repository contains code for our [Dart](https://dart.dev/) and [Flutter](ht
|
||||
|
||||
Stream allows developers to rapidly deploy scalable feeds and chat messaging with an industry leading 99.999% uptime SLA guarantee.
|
||||
|
||||
## Sample apps and demos
|
||||
Our team maintains a dedicated repository for fully-fledged sample applications and demos. Consider checking out [GetStream/flutter-samples](https://github.com/GetStream/flutter-samples) to learn more or get started by looking at our latest [Stream Chat demo](https://github.com/GetStream/flutter-samples/tree/main/stream_chat_v1).
|
||||
|
||||
## Structure
|
||||
Stream Chat Dart is a monorepo built using [Melos](https://docs.page/invertase/melos). Individual packages can be found in the `packages` directory while configuration and top level commands can be found in `melos.yaml`.
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
## 1.4.0-beta
|
||||
|
||||
- Improved attachment uploading
|
||||
- Fix: update member presence
|
||||
- Added skip_push to message model
|
||||
- Minor fixes and improvements
|
||||
|
||||
## 1.3.2+1-beta
|
||||
|
||||
- Fixed queryChannels bug
|
||||
|
||||
@@ -1,63 +1,147 @@
|
||||
include: package:pedantic/analysis_options.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
exclude:
|
||||
- lib/**/*.g.dart
|
||||
- lib/**/*.freezed.dart
|
||||
- example/*
|
||||
- test/*
|
||||
|
||||
linter:
|
||||
rules:
|
||||
# these rules are documented on and in the same order as
|
||||
# the Dart Lint rules page to make maintenance easier
|
||||
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
|
||||
# - always_declare_return_types
|
||||
# - always_specify_types
|
||||
# - annotate_overrides
|
||||
# - avoid_as
|
||||
linter:
|
||||
rules:
|
||||
- always_use_package_imports
|
||||
- avoid_empty_else
|
||||
- avoid_init_to_null
|
||||
- avoid_return_types_on_setters
|
||||
- avoid_web_libraries_in_flutter
|
||||
- await_only_futures
|
||||
- camel_case_types
|
||||
- avoid_relative_lib_imports
|
||||
- avoid_slow_async_io
|
||||
- avoid_types_as_parameter_names
|
||||
- cancel_subscriptions
|
||||
- close_sinks
|
||||
# - comment_references # we do not presume as to what people want to reference in their dartdocs
|
||||
# - constant_identifier_names # https://github.com/dart-lang/linter/issues/204
|
||||
- control_flow_in_finally
|
||||
- empty_constructor_bodies
|
||||
- diagnostic_describe_all_properties
|
||||
- empty_statements
|
||||
- hash_and_equals
|
||||
- implementation_imports
|
||||
# - invariant_booleans
|
||||
# - iterable_contains_unrelated_type
|
||||
- library_names
|
||||
# - library_prefixes
|
||||
# - list_remove_unrelated_type
|
||||
# - literal_only_boolean_expressions
|
||||
- non_constant_identifier_names
|
||||
# - one_member_abstracts
|
||||
# - only_throw_errors
|
||||
# - overridden_fields
|
||||
- package_api_docs
|
||||
- package_names
|
||||
- package_prefixed_library_names
|
||||
- prefer_is_not_empty
|
||||
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
|
||||
- public_member_api_docs
|
||||
- slash_for_doc_comments
|
||||
# - sort_constructors_first
|
||||
# - sort_unnamed_constructors_first
|
||||
# - super_goes_last # no longer needed w/ Dart 2
|
||||
- invariant_booleans
|
||||
- iterable_contains_unrelated_type
|
||||
- list_remove_unrelated_type
|
||||
- literal_only_boolean_expressions
|
||||
- no_adjacent_strings_in_list
|
||||
- no_duplicate_case_values
|
||||
- no_logic_in_create_state
|
||||
- prefer_void_to_null
|
||||
- test_types_in_equals
|
||||
- throw_in_finally
|
||||
# - type_annotate_public_apis # subset of always_specify_types
|
||||
- type_init_formals
|
||||
# - unawaited_futures
|
||||
- unnecessary_brace_in_string_interps
|
||||
- unnecessary_getters_setters
|
||||
- unnecessary_statements
|
||||
- unrelated_type_equality_checks
|
||||
- valid_regexps
|
||||
- omit_local_variable_types
|
||||
- use_key_in_widget_constructors
|
||||
- valid_regexps
|
||||
- always_declare_return_types
|
||||
- always_put_required_named_parameters_first
|
||||
- always_require_non_null_named_parameters
|
||||
- annotate_overrides
|
||||
- avoid_bool_literals_in_conditional_expressions
|
||||
- avoid_catching_errors
|
||||
- avoid_init_to_null
|
||||
- avoid_null_checks_in_equality_operators
|
||||
- avoid_positional_boolean_parameters
|
||||
- avoid_private_typedef_functions
|
||||
- avoid_redundant_argument_values
|
||||
- avoid_return_types_on_setters
|
||||
- avoid_returning_null
|
||||
- avoid_returning_null_for_void
|
||||
- avoid_shadowing_type_parameters
|
||||
- avoid_single_cascade_in_expression_statements
|
||||
- avoid_unnecessary_containers
|
||||
- avoid_unused_constructor_parameters
|
||||
- await_only_futures
|
||||
- camel_case_extensions
|
||||
- camel_case_types
|
||||
- cascade_invocations
|
||||
|
||||
- constant_identifier_names
|
||||
- curly_braces_in_flow_control_structures
|
||||
- directives_ordering
|
||||
- empty_catches
|
||||
- empty_constructor_bodies
|
||||
- exhaustive_cases
|
||||
- file_names
|
||||
- implementation_imports
|
||||
- join_return_with_assignment
|
||||
- leading_newlines_in_multiline_strings
|
||||
- library_names
|
||||
- library_prefixes
|
||||
- lines_longer_than_80_chars
|
||||
- missing_whitespace_between_adjacent_strings
|
||||
- non_constant_identifier_names
|
||||
- null_closures
|
||||
- one_member_abstracts
|
||||
- only_throw_errors
|
||||
- package_api_docs
|
||||
- package_prefixed_library_names
|
||||
- parameter_assignments
|
||||
- prefer_adjacent_string_concatenation
|
||||
- prefer_asserts_in_initializer_lists
|
||||
- prefer_asserts_with_message
|
||||
- prefer_collection_literals
|
||||
- prefer_conditional_assignment
|
||||
- prefer_const_constructors
|
||||
- prefer_const_constructors_in_immutables
|
||||
- prefer_const_declarations
|
||||
- prefer_const_literals_to_create_immutables
|
||||
- prefer_constructors_over_static_methods
|
||||
- prefer_contains
|
||||
- prefer_equal_for_default_values
|
||||
- prefer_expression_function_bodies
|
||||
- prefer_final_fields
|
||||
- prefer_final_in_for_each
|
||||
- prefer_final_locals
|
||||
- prefer_function_declarations_over_variables
|
||||
- prefer_generic_function_type_aliases
|
||||
- prefer_if_elements_to_conditional_expressions
|
||||
- prefer_if_null_operators
|
||||
- prefer_initializing_formals
|
||||
- prefer_inlined_adds
|
||||
- prefer_int_literals
|
||||
- prefer_interpolation_to_compose_strings
|
||||
- prefer_is_empty
|
||||
- prefer_is_not_empty
|
||||
- prefer_is_not_operator
|
||||
- prefer_null_aware_operators
|
||||
- prefer_single_quotes
|
||||
- prefer_spread_collections
|
||||
- prefer_typing_uninitialized_variables
|
||||
- provide_deprecation_message
|
||||
- public_member_api_docs
|
||||
- recursive_getters
|
||||
- sized_box_for_whitespace
|
||||
- slash_for_doc_comments
|
||||
- sort_child_properties_last
|
||||
- sort_constructors_first
|
||||
- sort_unnamed_constructors_first
|
||||
|
||||
- type_annotate_public_apis
|
||||
- type_init_formals
|
||||
- unnecessary_await_in_return
|
||||
- unnecessary_brace_in_string_interps
|
||||
- unnecessary_const
|
||||
- unnecessary_getters_setters
|
||||
- unnecessary_lambdas
|
||||
- unnecessary_new
|
||||
- unnecessary_null_aware_assignments
|
||||
- unnecessary_null_in_if_null_operators
|
||||
- unnecessary_nullable_for_final_variable_declarations
|
||||
- unnecessary_parenthesis
|
||||
- unnecessary_raw_strings
|
||||
- unnecessary_string_escapes
|
||||
- unnecessary_string_interpolations
|
||||
- unnecessary_this
|
||||
- use_is_even_rather_than_modulo
|
||||
- use_late_for_private_fields_and_variables
|
||||
- use_rethrow_when_possible
|
||||
- use_setters_to_change_properties
|
||||
- use_to_and_as_if_applicable
|
||||
- package_names
|
||||
- sort_pub_dependencies
|
||||
|
||||
# To be added when null-safe:
|
||||
# - cast_nullable_to_non_nullable
|
||||
#- unnecessary_null_checks
|
||||
# - tighten_type_of_initializing_formals
|
||||
# - null_check_on_nullable_type_parameter
|
||||
@@ -2,24 +2,16 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:pedantic/pedantic.dart' show unawaited;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/src/api/retry_queue.dart';
|
||||
import 'package:stream_chat/src/debounce.dart';
|
||||
import 'package:stream_chat/src/event_type.dart';
|
||||
import 'package:stream_chat/src/models/attachment_file.dart';
|
||||
import 'package:stream_chat/src/models/channel_state.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
import '../client.dart';
|
||||
import '../models/event.dart';
|
||||
import '../models/member.dart';
|
||||
import '../models/message.dart';
|
||||
import 'requests.dart';
|
||||
import 'responses.dart';
|
||||
import 'package:stream_chat/src/extensions/rate_limit.dart';
|
||||
|
||||
/// This a the class that manages a specific channel.
|
||||
class Channel {
|
||||
@@ -57,7 +49,8 @@ class Channel {
|
||||
set extraData(Map<String, dynamic> extraData) {
|
||||
if (_initializedCompleter.isCompleted) {
|
||||
throw Exception(
|
||||
'Once the channel is initialized you should use channel.update to update channel data');
|
||||
'Once the channel is initialized you should use channel.update '
|
||||
'to update channel data');
|
||||
}
|
||||
_extraData = extraData;
|
||||
}
|
||||
@@ -168,14 +161,15 @@ class Channel {
|
||||
final Completer<bool> _initializedCompleter = Completer();
|
||||
|
||||
/// True if this is initialized
|
||||
/// Call [watch] to initialize the client or instantiate it using [Channel.fromState]
|
||||
/// Call [watch] to initialize the client or instantiate it using
|
||||
/// [Channel.fromState]
|
||||
Future<bool> get initialized => _initializedCompleter.future;
|
||||
|
||||
final _cancelableAttachmentUploadRequest = <String, CancelToken>{};
|
||||
final _messageAttachmentsUploadCompleter = <String, Completer>{};
|
||||
|
||||
/// Cancels [attachmentId] upload request. Throws exception if the request hasn't
|
||||
/// even started yet, Already completed or Already cancelled.
|
||||
/// Cancels [attachmentId] upload request. Throws exception if the request
|
||||
/// hasn't even started yet, Already completed or Already cancelled.
|
||||
///
|
||||
/// Optionally, provide a [reason] for the cancellation.
|
||||
void cancelAttachmentUpload(
|
||||
@@ -185,7 +179,8 @@ class Channel {
|
||||
final cancelToken = _cancelableAttachmentUploadRequest[attachmentId];
|
||||
if (cancelToken == null) {
|
||||
throw Exception(
|
||||
"Upload request for this Attachment hasn't started yet or else Already completed",
|
||||
"Upload request for this Attachment hasn't started yet or else "
|
||||
'Already completed',
|
||||
);
|
||||
}
|
||||
if (cancelToken.isCancelled) throw Exception('Already cancelled');
|
||||
@@ -193,15 +188,14 @@ class Channel {
|
||||
}
|
||||
|
||||
/// Retries the failed [attachmentId] upload request.
|
||||
Future<void> retryAttachmentUpload(String messageId, String attachmentId) {
|
||||
return _uploadAttachments(messageId, [attachmentId]);
|
||||
}
|
||||
Future<void> retryAttachmentUpload(String messageId, String attachmentId) =>
|
||||
_uploadAttachments(messageId, [attachmentId]);
|
||||
|
||||
Future<void> _uploadAttachments(
|
||||
String messageId,
|
||||
Iterable<String> attachmentIds,
|
||||
) {
|
||||
var message = state.messages.firstWhere(
|
||||
final message = state.messages.firstWhere(
|
||||
(it) => it.id == messageId,
|
||||
orElse: () => null,
|
||||
);
|
||||
@@ -224,29 +218,29 @@ class Channel {
|
||||
}
|
||||
|
||||
client.logger.info('Found ${attachments.length} attachments');
|
||||
|
||||
void updateAttachment(Attachment attachment) {
|
||||
final index =
|
||||
message.attachments.indexWhere((it) => it.id == attachment.id);
|
||||
if (index != -1) {
|
||||
message.attachments[index] = attachment;
|
||||
state?.addMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
return Future.wait(attachments.map((it) {
|
||||
client.logger.info('Uploading ${it.id} attachment...');
|
||||
|
||||
void updateAttachment(Attachment attachment) {
|
||||
final index = message.attachments.indexWhere((it) {
|
||||
return it.id == attachment.id;
|
||||
});
|
||||
if (index != -1) {
|
||||
message.attachments[index] = attachment;
|
||||
state?.addMessage(message);
|
||||
}
|
||||
}
|
||||
final throttledUpdateAttachment = updateAttachment.throttled(
|
||||
const Duration(milliseconds: 500),
|
||||
);
|
||||
|
||||
void onSendProgress(int sent, int total) {
|
||||
debounce(
|
||||
timeout: Duration(seconds: 1),
|
||||
target: updateAttachment,
|
||||
positionalArguments: [
|
||||
it.copyWith(
|
||||
uploadState: UploadState.inProgress(uploaded: sent, total: total),
|
||||
),
|
||||
],
|
||||
);
|
||||
throttledUpdateAttachment([
|
||||
it.copyWith(
|
||||
uploadState: UploadState.inProgress(uploaded: sent, total: total),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
final isImage = it.type == 'image';
|
||||
@@ -270,18 +264,26 @@ class Channel {
|
||||
client.logger.info('Attachment ${it.id} uploaded successfully...');
|
||||
if (isImage) {
|
||||
updateAttachment(
|
||||
it.copyWith(imageUrl: url, uploadState: UploadState.success()),
|
||||
it.copyWith(
|
||||
imageUrl: url,
|
||||
uploadState: const UploadState.success(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
updateAttachment(
|
||||
it.copyWith(assetUrl: url, uploadState: UploadState.success()),
|
||||
it.copyWith(
|
||||
assetUrl: url,
|
||||
uploadState: const UploadState.success(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}).catchError((e, stk) {
|
||||
client.logger.severe('error uploading the attachment', e, stk);
|
||||
updateAttachment(
|
||||
it.copyWith(uploadState: UploadState.failed(error: e.toString())),
|
||||
);
|
||||
}).whenComplete(() {
|
||||
throttledUpdateAttachment?.cancel();
|
||||
_cancelableAttachmentUploadRequest.remove(it.id);
|
||||
});
|
||||
})).whenComplete(() {
|
||||
@@ -305,6 +307,7 @@ class Channel {
|
||||
(m) => m.id == message?.quotedMessageId,
|
||||
orElse: () => null,
|
||||
);
|
||||
// ignore: parameter_assignments
|
||||
message = message.copyWith(
|
||||
createdAt: message.createdAt ?? DateTime.now(),
|
||||
user: _client.state.user,
|
||||
@@ -313,7 +316,7 @@ class Channel {
|
||||
attachments: message.attachments?.map(
|
||||
(it) {
|
||||
if (it.uploadState.isSuccess) return it;
|
||||
return it.copyWith(uploadState: UploadState.preparing());
|
||||
return it.copyWith(uploadState: const UploadState.preparing());
|
||||
},
|
||||
)?.toList(),
|
||||
);
|
||||
@@ -335,11 +338,13 @@ class Channel {
|
||||
_messageAttachmentsUploadCompleter[message.id] =
|
||||
attachmentsUploadCompleter;
|
||||
|
||||
unawaited(_uploadAttachments(
|
||||
// ignore: unawaited_futures
|
||||
_uploadAttachments(
|
||||
message.id,
|
||||
message.attachments.map((it) => it.id),
|
||||
));
|
||||
);
|
||||
|
||||
// ignore: parameter_assignments
|
||||
message = await attachmentsUploadCompleter.future;
|
||||
}
|
||||
|
||||
@@ -364,13 +369,14 @@ class Channel {
|
||||
.remove(message.id)
|
||||
?.completeError('Message Cancelled');
|
||||
|
||||
// ignore: parameter_assignments
|
||||
message = message.copyWith(
|
||||
status: MessageSendingStatus.updating,
|
||||
updatedAt: message.updatedAt ?? DateTime.now(),
|
||||
attachments: message.attachments?.map(
|
||||
(it) {
|
||||
if (it.uploadState.isSuccess) return it;
|
||||
return it.copyWith(uploadState: UploadState.preparing());
|
||||
return it.copyWith(uploadState: const UploadState.preparing());
|
||||
},
|
||||
)?.toList(),
|
||||
);
|
||||
@@ -383,11 +389,13 @@ class Channel {
|
||||
_messageAttachmentsUploadCompleter[message.id] =
|
||||
attachmentsUploadCompleter;
|
||||
|
||||
unawaited(_uploadAttachments(
|
||||
// ignore: unawaited_futures
|
||||
_uploadAttachments(
|
||||
message.id,
|
||||
message.attachments.map((it) => it.id),
|
||||
));
|
||||
);
|
||||
|
||||
// ignore: parameter_assignments
|
||||
message = await attachmentsUploadCompleter.future;
|
||||
}
|
||||
|
||||
@@ -423,6 +431,7 @@ class Channel {
|
||||
}
|
||||
|
||||
try {
|
||||
// ignore: parameter_assignments
|
||||
message = message.copyWith(
|
||||
type: 'deleted',
|
||||
status: MessageSendingStatus.deleting,
|
||||
@@ -456,7 +465,7 @@ class Channel {
|
||||
throw ArgumentError('Invalid timeout or Expiration date');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}(), 'Check for invalid token or expiration date');
|
||||
|
||||
DateTime pinExpires;
|
||||
if (timeoutOrExpirationDate is DateTime) {
|
||||
@@ -475,39 +484,36 @@ class Channel {
|
||||
}
|
||||
|
||||
/// Unpins provided message
|
||||
Future<UpdateMessageResponse> unpinMessage(Message message) {
|
||||
return updateMessage(message.copyWith(pinned: false));
|
||||
}
|
||||
Future<UpdateMessageResponse> unpinMessage(Message message) =>
|
||||
updateMessage(message.copyWith(pinned: false));
|
||||
|
||||
/// Send a file to this channel
|
||||
Future<SendFileResponse> sendFile(
|
||||
AttachmentFile file, {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) {
|
||||
return _client.sendFile(
|
||||
file,
|
||||
id,
|
||||
type,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
_client.sendFile(
|
||||
file,
|
||||
id,
|
||||
type,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
|
||||
/// Send an image to this channel
|
||||
Future<SendImageResponse> sendImage(
|
||||
AttachmentFile file, {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) {
|
||||
return _client.sendImage(
|
||||
file,
|
||||
id,
|
||||
type,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
_client.sendImage(
|
||||
file,
|
||||
id,
|
||||
type,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
|
||||
/// A message search.
|
||||
Future<SearchMessagesResponse> search({
|
||||
@@ -515,35 +521,32 @@ class Channel {
|
||||
Map<String, dynamic> messageFilters,
|
||||
List<SortOption> sort,
|
||||
PaginationParams paginationParams,
|
||||
}) {
|
||||
return _client.search(
|
||||
{
|
||||
'cid': {
|
||||
r'$in': [cid],
|
||||
}) =>
|
||||
_client.search(
|
||||
{
|
||||
'cid': {
|
||||
r'$in': [cid],
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: sort,
|
||||
query: query,
|
||||
paginationParams: paginationParams,
|
||||
messageFilters: messageFilters,
|
||||
);
|
||||
}
|
||||
sort: sort,
|
||||
query: query,
|
||||
paginationParams: paginationParams,
|
||||
messageFilters: messageFilters,
|
||||
);
|
||||
|
||||
/// Delete a file from this channel
|
||||
Future<EmptyResponse> deleteFile(
|
||||
String url, {
|
||||
CancelToken cancelToken,
|
||||
}) {
|
||||
return _client.deleteFile(url, id, type, cancelToken: cancelToken);
|
||||
}
|
||||
}) =>
|
||||
_client.deleteFile(url, id, type, cancelToken: cancelToken);
|
||||
|
||||
/// Delete an image from this channel
|
||||
Future<EmptyResponse> deleteImage(
|
||||
String url, {
|
||||
CancelToken cancelToken,
|
||||
}) {
|
||||
return _client.deleteImage(url, id, type, cancelToken: cancelToken);
|
||||
}
|
||||
}) =>
|
||||
_client.deleteImage(url, id, type, cancelToken: cancelToken);
|
||||
|
||||
/// Send an event on this channel
|
||||
Future<EmptyResponse> sendEvent(Event event) {
|
||||
@@ -551,9 +554,7 @@ class Channel {
|
||||
return _client.post(
|
||||
'$_channelURL/event',
|
||||
data: {'event': event.toJson()},
|
||||
).then((res) {
|
||||
return _client.decode(res.data, EmptyResponse.fromJson);
|
||||
});
|
||||
).then((res) => _client.decode(res.data, EmptyResponse.fromJson));
|
||||
}
|
||||
|
||||
/// Send a reaction to this channel
|
||||
@@ -643,11 +644,10 @@ class Channel {
|
||||
}
|
||||
|
||||
final latestReactions = [...message.latestReactions ?? <Reaction>[]]
|
||||
..removeWhere((r) {
|
||||
return r.userId == reaction.userId &&
|
||||
r.type == reaction.type &&
|
||||
r.messageId == reaction.messageId;
|
||||
});
|
||||
..removeWhere((r) =>
|
||||
r.userId == reaction.userId &&
|
||||
r.type == reaction.type &&
|
||||
r.messageId == reaction.messageId);
|
||||
|
||||
final ownReactions = [...latestReactions ?? <Reaction>[]]
|
||||
..removeWhere((it) => it.userId != user.id);
|
||||
@@ -825,7 +825,7 @@ class Channel {
|
||||
})
|
||||
..addAll(options);
|
||||
|
||||
var response;
|
||||
ChannelState response;
|
||||
|
||||
try {
|
||||
response = await query(options: watchOptions);
|
||||
@@ -861,7 +861,8 @@ class Channel {
|
||||
}
|
||||
|
||||
/// List the message replies for a parent message
|
||||
/// Set [preferOffline] to true to avoid the api call if the data is already in the offline storage
|
||||
/// Set [preferOffline] to true to avoid the api call if the data is already
|
||||
/// in the offline storage
|
||||
Future<QueryRepliesResponse> getReplies(
|
||||
String parentId,
|
||||
PaginationParams options, {
|
||||
@@ -940,16 +941,15 @@ class Channel {
|
||||
}
|
||||
|
||||
/// Creates a new channel
|
||||
Future<ChannelState> create() async {
|
||||
return query(options: {
|
||||
'watch': false,
|
||||
'state': false,
|
||||
'presence': false,
|
||||
});
|
||||
}
|
||||
Future<ChannelState> create() async => query(options: {
|
||||
'watch': false,
|
||||
'state': false,
|
||||
'presence': false,
|
||||
});
|
||||
|
||||
/// Query the API, get messages, members or other channel fields
|
||||
/// Set [preferOffline] to true to avoid the api call if the data is already in the offline storage
|
||||
/// Set [preferOffline] to true to avoid the api call if the data is already
|
||||
/// in the offline storage
|
||||
Future<ChannelState> query({
|
||||
Map<String, dynamic> options = const {},
|
||||
PaginationParams messagesPagination,
|
||||
@@ -1112,8 +1112,9 @@ class Channel {
|
||||
});
|
||||
}
|
||||
|
||||
/// Hides the channel from [StreamChatClient.queryChannels] for the user until a message is added
|
||||
/// If [clearHistory] is set to true - all messages will be removed for the user
|
||||
/// Hides the channel from [StreamChatClient.queryChannels] for the user
|
||||
/// until a message is added If [clearHistory] is set to true - all messages
|
||||
/// will be removed for the user
|
||||
Future<EmptyResponse> hide({bool clearHistory = false}) async {
|
||||
_checkInitialized();
|
||||
final response = await _client
|
||||
@@ -1134,28 +1135,28 @@ class Channel {
|
||||
return _client.decode(response.data, EmptyResponse.fromJson);
|
||||
}
|
||||
|
||||
/// Stream of [Event] coming from websocket connection specific for the channel
|
||||
/// Pass an eventType as parameter in order to filter just a type of event
|
||||
/// Stream of [Event] coming from websocket connection specific for the
|
||||
/// channel. Pass an eventType as parameter in order to filter just a type
|
||||
/// of event
|
||||
Stream<Event> on([
|
||||
String eventType,
|
||||
String eventType2,
|
||||
String eventType3,
|
||||
String eventType4,
|
||||
]) {
|
||||
return _client
|
||||
.on(
|
||||
eventType,
|
||||
eventType2,
|
||||
eventType3,
|
||||
eventType4,
|
||||
)
|
||||
.where((e) => e.cid == cid);
|
||||
}
|
||||
]) =>
|
||||
_client
|
||||
.on(
|
||||
eventType,
|
||||
eventType2,
|
||||
eventType3,
|
||||
eventType4,
|
||||
)
|
||||
.where((e) => e.cid == cid);
|
||||
|
||||
DateTime _lastTypingEvent;
|
||||
|
||||
/// First of the [EventType.typingStart] and [EventType.typingStop] events based on the users keystrokes.
|
||||
/// Call this on every keystroke.
|
||||
/// First of the [EventType.typingStart] and [EventType.typingStop] events
|
||||
/// based on the users keystrokes. Call this on every keystroke.
|
||||
Future<void> keyStroke([String parentId]) async {
|
||||
if (config?.typingEvents == false) {
|
||||
return;
|
||||
@@ -1196,17 +1197,21 @@ class Channel {
|
||||
void _checkInitialized() {
|
||||
if (!_initializedCompleter.isCompleted) {
|
||||
throw Exception(
|
||||
"Channel $cid hasn't been initialized yet. Make sure to call .watch() or to instantiate the client using [Channel.fromState]");
|
||||
"Channel $cid hasn't been initialized yet. Make sure to call .watch()"
|
||||
' or to instantiate the client using [Channel.fromState]');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The class that handles the state of the channel listening to the events
|
||||
class ChannelClientState {
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
|
||||
/// Creates a new instance listening to events and updating the state
|
||||
ChannelClientState(this._channel, ChannelState channelState) {
|
||||
ChannelClientState(
|
||||
this._channel,
|
||||
ChannelState channelState,
|
||||
) : _debouncedUpdatePersistenceChannelState = _channel
|
||||
?._client?.chatPersistenceClient?.updateChannelState
|
||||
?.debounced(const Duration(seconds: 1)) {
|
||||
retryQueue = RetryQueue(
|
||||
channel: _channel,
|
||||
logger: Logger('RETRY QUEUE ${_channel.cid}'),
|
||||
@@ -1252,14 +1257,16 @@ class ChannelClientState {
|
||||
_channel._client.chatPersistenceClient
|
||||
?.getChannelStateByCid(_channel.cid)
|
||||
?.then((state) {
|
||||
// Replacing the persistence state members with the latest `channelState.members`
|
||||
// as they may have changes over the time.
|
||||
// Replacing the persistence state members with the latest
|
||||
// `channelState.members` as they may have changes over the time.
|
||||
updateChannelState(state.copyWith(members: channelState.members));
|
||||
retryFailedMessages();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
|
||||
void _computeInitialUnread() {
|
||||
final userRead = channelState?.read?.firstWhere(
|
||||
(r) => r.user.id == _channel._client.state?.user?.id,
|
||||
@@ -1277,11 +1284,16 @@ class ChannelClientState {
|
||||
m.attachments?.isNotEmpty == true &&
|
||||
m.attachments?.any((e) {
|
||||
final url = e.imageUrl ?? e.assetUrl;
|
||||
if (url == null || !url.contains('stream-io-cdn.com')) {
|
||||
if (url == null || !url.contains('')) {
|
||||
return false;
|
||||
}
|
||||
final uri = Uri.parse(url);
|
||||
if (uri.host != 'stream-io-cdn.com' ||
|
||||
uri.queryParameters['Expires'] == null) {
|
||||
return false;
|
||||
}
|
||||
final expiration =
|
||||
DateTime.parse(Uri.parse(url).queryParameters['Expires']);
|
||||
DateTime.parse(uri.queryParameters['Expires']);
|
||||
return expiration.isBefore(DateTime.now());
|
||||
}) ==
|
||||
true)
|
||||
@@ -1338,8 +1350,8 @@ class ChannelClientState {
|
||||
|
||||
/// Flag which indicates if [ChannelClientState] contain latest/recent messages or not.
|
||||
/// This flag should be managed by UI sdks.
|
||||
/// When false, any new message (received by WebSocket event - [EventType.messageNew]) will not
|
||||
/// be pushed on to message list.
|
||||
/// When false, any new message (received by WebSocket event
|
||||
/// - [EventType.messageNew]) will not be pushed on to message list.
|
||||
bool get isUpToDate => _isUpToDateController.value;
|
||||
|
||||
set isUpToDate(bool isUpToDate) => _isUpToDateController.add(isUpToDate);
|
||||
@@ -1357,12 +1369,18 @@ class ChannelClientState {
|
||||
Future<void> retryFailedMessages() async {
|
||||
final failedMessages =
|
||||
<Message>[...messages, ...threads.values.expand((v) => v)]
|
||||
.where((message) =>
|
||||
message.status != null &&
|
||||
message.status != MessageSendingStatus.sent &&
|
||||
message.createdAt.isBefore(DateTime.now().subtract(Duration(
|
||||
seconds: 1,
|
||||
))))
|
||||
.where(
|
||||
(message) =>
|
||||
message.status != null &&
|
||||
message.status != MessageSendingStatus.sent &&
|
||||
message.createdAt.isBefore(
|
||||
DateTime.now().subtract(
|
||||
const Duration(
|
||||
seconds: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
retryQueue.add(failedMessages);
|
||||
@@ -1471,27 +1489,32 @@ class ChannelClientState {
|
||||
return;
|
||||
}
|
||||
|
||||
_subscriptions.add(_channel
|
||||
.on(
|
||||
EventType.messageRead,
|
||||
EventType.notificationMarkRead,
|
||||
)
|
||||
.listen((event) {
|
||||
final readList = List<Read>.from(_channelState?.read ?? []);
|
||||
final userReadIndex = read?.indexWhere((r) => r.user.id == event.user.id);
|
||||
_subscriptions.add(
|
||||
_channel
|
||||
.on(
|
||||
EventType.messageRead,
|
||||
EventType.notificationMarkRead,
|
||||
)
|
||||
.listen(
|
||||
(event) {
|
||||
final readList = List<Read>.from(_channelState?.read ?? []);
|
||||
final userReadIndex =
|
||||
read?.indexWhere((r) => r.user.id == event.user.id);
|
||||
|
||||
if (userReadIndex != null && userReadIndex != -1) {
|
||||
final userRead = readList.removeAt(userReadIndex);
|
||||
if (userRead.user?.id == _channel._client.state.user.id) {
|
||||
_unreadCountController.add(0);
|
||||
}
|
||||
readList.add(Read(
|
||||
user: event.user,
|
||||
lastRead: event.createdAt,
|
||||
));
|
||||
_channelState = _channelState.copyWith(read: readList);
|
||||
}
|
||||
}));
|
||||
if (userReadIndex != null && userReadIndex != -1) {
|
||||
final userRead = readList.removeAt(userReadIndex);
|
||||
if (userRead.user?.id == _channel._client.state.user.id) {
|
||||
_unreadCountController.add(0);
|
||||
}
|
||||
readList.add(Read(
|
||||
user: event.user,
|
||||
lastRead: event.createdAt,
|
||||
));
|
||||
_channelState = _channelState.copyWith(read: readList);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Channel message list
|
||||
@@ -1527,11 +1550,8 @@ class ChannelClientState {
|
||||
List<Member>, Map<String, User>, List<Member>>(
|
||||
channelStateStream.map((cs) => cs.members),
|
||||
_channel.client.state.usersStream,
|
||||
(members, users) {
|
||||
return members
|
||||
.map((e) => e.copyWith(user: users[e.user.id]))
|
||||
.toList();
|
||||
},
|
||||
(members, users) =>
|
||||
members.map((e) => e.copyWith(user: users[e.user.id])).toList(),
|
||||
);
|
||||
|
||||
/// Channel watcher count
|
||||
@@ -1551,9 +1571,7 @@ class ChannelClientState {
|
||||
CombineLatestStream.combine2<List<User>, Map<String, User>, List<User>>(
|
||||
channelStateStream.map((cs) => cs.watchers),
|
||||
_channel.client.state.usersStream,
|
||||
(watchers, users) {
|
||||
return watchers.map((e) => users[e.id] ?? e).toList();
|
||||
},
|
||||
(watchers, users) => watchers.map((e) => users[e.id] ?? e).toList(),
|
||||
);
|
||||
|
||||
/// Channel read list
|
||||
@@ -1625,9 +1643,7 @@ class ChannelClientState {
|
||||
true)
|
||||
?.toList() ??
|
||||
[],
|
||||
];
|
||||
|
||||
newMessages.sort(_sortByCreatedAt);
|
||||
]..sort(_sortByCreatedAt);
|
||||
|
||||
final newWatchers = <User>[
|
||||
...updatedState?.watchers ?? [],
|
||||
@@ -1690,16 +1706,11 @@ class ChannelClientState {
|
||||
ChannelState get channelState => _channelStateController.value;
|
||||
BehaviorSubject<ChannelState> _channelStateController;
|
||||
|
||||
final Debounce _debouncedUpdatePersistenceChannelState;
|
||||
|
||||
set _channelState(ChannelState v) {
|
||||
_channelStateController.add(v);
|
||||
|
||||
if (_channel._client.persistenceEnabled) {
|
||||
debounce(
|
||||
timeout: Duration(milliseconds: 500),
|
||||
target: _channel._client.chatPersistenceClient?.updateChannelState,
|
||||
positionalArguments: [v],
|
||||
);
|
||||
}
|
||||
_debouncedUpdatePersistenceChannelState?.call([v]);
|
||||
}
|
||||
|
||||
/// The channel threads related to this channel
|
||||
@@ -1735,28 +1746,62 @@ class ChannelClientState {
|
||||
return;
|
||||
}
|
||||
|
||||
_subscriptions.add(_channel.on(EventType.typingStart).listen((event) {
|
||||
if (event.user.id != _channel.client.state.user.id) {
|
||||
_typings[event.user] = DateTime.now();
|
||||
_typingEventsController.add(_typings.keys.toList());
|
||||
}
|
||||
}));
|
||||
|
||||
_subscriptions.add(_channel.on(EventType.typingStop).listen((event) {
|
||||
if (event.user.id != _channel.client.state.user.id) {
|
||||
_typings.remove(event.user);
|
||||
_typingEventsController.add(_typings.keys.toList());
|
||||
}
|
||||
}));
|
||||
_subscriptions
|
||||
..add(
|
||||
_channel.on(EventType.typingStart).listen(
|
||||
(event) {
|
||||
if (event.user.id != _channel.client.state.user.id) {
|
||||
_typings[event.user] = DateTime.now();
|
||||
_typingEventsController.add(_typings.keys.toList());
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
..add(
|
||||
_channel.on(EventType.typingStop).listen(
|
||||
(event) {
|
||||
if (event.user.id != _channel.client.state.user.id) {
|
||||
_typings.remove(event.user);
|
||||
_typingEventsController.add(_typings.keys.toList());
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
..add(
|
||||
_channel
|
||||
.on()
|
||||
.where((event) =>
|
||||
event.user != null &&
|
||||
members?.any((m) => m.userId == event.user.id) == true)
|
||||
.listen(
|
||||
(event) {
|
||||
final newMembers = List<Member>.from(members);
|
||||
final oldMemberIndex =
|
||||
newMembers.indexWhere((m) => m.userId == event.user.id);
|
||||
if (oldMemberIndex > -1) {
|
||||
final oldMember = newMembers.removeAt(oldMemberIndex);
|
||||
updateChannelState(ChannelState(
|
||||
members: [
|
||||
...newMembers,
|
||||
oldMember.copyWith(
|
||||
user: event.user,
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Timer _cleaningTimer;
|
||||
|
||||
void _startCleaning() {
|
||||
if (_channel.config?.typingEvents == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
_cleaningTimer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||
_cleaningTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (_channel._lastTypingEvent != null &&
|
||||
@@ -1769,8 +1814,9 @@ class ChannelClientState {
|
||||
}
|
||||
|
||||
Timer _pinnedMessagesTimer;
|
||||
|
||||
void _startCleaningPinnedMessages() {
|
||||
_pinnedMessagesTimer = Timer.periodic(Duration(seconds: 30), (_) {
|
||||
_pinnedMessagesTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
final now = DateTime.now();
|
||||
var expiredMessages = channelState.pinnedMessages
|
||||
?.where((m) => m.pinExpires?.isBefore(now) == true)
|
||||
@@ -1781,8 +1827,6 @@ class ChannelClientState {
|
||||
.map((m) => m.copyWith(
|
||||
pinExpires: null,
|
||||
pinned: false,
|
||||
pinnedAt: null,
|
||||
pinnedBy: null,
|
||||
))
|
||||
.toList();
|
||||
|
||||
@@ -1811,6 +1855,7 @@ class ChannelClientState {
|
||||
|
||||
/// Call this method to dispose this object
|
||||
void dispose() {
|
||||
_debouncedUpdatePersistenceChannelState?.cancel();
|
||||
_unreadCountController.close();
|
||||
retryQueue.dispose();
|
||||
_subscriptions.forEach((s) => s.cancel());
|
||||
|
||||
@@ -5,22 +5,6 @@ part 'requests.g.dart';
|
||||
/// Sorting options
|
||||
@JsonSerializable(createFactory: false)
|
||||
class SortOption<T> {
|
||||
/// Ascending order
|
||||
static const ASC = 1;
|
||||
|
||||
/// Descending order
|
||||
static const DESC = -1;
|
||||
|
||||
/// A sorting field name
|
||||
final String field;
|
||||
|
||||
/// A sorting direction
|
||||
final int direction;
|
||||
|
||||
/// Sorting field Comparator required for offline sorting
|
||||
@JsonKey(ignore: true)
|
||||
final Comparator<T> comparator;
|
||||
|
||||
/// Creates a new SortOption instance
|
||||
///
|
||||
/// For example:
|
||||
@@ -34,6 +18,24 @@ class SortOption<T> {
|
||||
this.comparator,
|
||||
});
|
||||
|
||||
/// Ascending order
|
||||
// ignore: constant_identifier_names
|
||||
static const ASC = 1;
|
||||
|
||||
/// Descending order
|
||||
// ignore: constant_identifier_names
|
||||
static const DESC = -1;
|
||||
|
||||
/// A sorting field name
|
||||
final String field;
|
||||
|
||||
/// A sorting direction
|
||||
final int direction;
|
||||
|
||||
/// Sorting field Comparator required for offline sorting
|
||||
@JsonKey(ignore: true)
|
||||
final Comparator<T> comparator;
|
||||
|
||||
/// Serialize model to json
|
||||
Map<String, dynamic> toJson() => _$SortOptionToJson(this);
|
||||
}
|
||||
@@ -41,6 +43,25 @@ class SortOption<T> {
|
||||
/// Pagination options.
|
||||
@JsonSerializable(createFactory: false, includeIfNull: false)
|
||||
class PaginationParams {
|
||||
/// Creates a new PaginationParams instance
|
||||
///
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// // limit to 50
|
||||
/// final paginationParams = PaginationParams(limit: 50);
|
||||
///
|
||||
/// // limit to 50 with offset
|
||||
/// final paginationParams = PaginationParams(limit: 50, offset: 50);
|
||||
/// ```
|
||||
const PaginationParams({
|
||||
this.limit = 10,
|
||||
this.offset = 0,
|
||||
this.greaterThan,
|
||||
this.greaterThanOrEqual,
|
||||
this.lessThan,
|
||||
this.lessThanOrEqual,
|
||||
});
|
||||
|
||||
/// The amount of items requested from the APIs.
|
||||
final int limit;
|
||||
|
||||
@@ -63,25 +84,6 @@ class PaginationParams {
|
||||
@JsonKey(name: 'id_lte')
|
||||
final String lessThanOrEqual;
|
||||
|
||||
/// Creates a new PaginationParams instance
|
||||
///
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// // limit to 50
|
||||
/// final paginationParams = PaginationParams(limit: 50);
|
||||
///
|
||||
/// // limit to 50 with offset
|
||||
/// final paginationParams = PaginationParams(limit: 50, offset: 50);
|
||||
/// ```
|
||||
const PaginationParams({
|
||||
this.limit = 10,
|
||||
this.offset = 0,
|
||||
this.greaterThan,
|
||||
this.greaterThanOrEqual,
|
||||
this.lessThan,
|
||||
this.lessThanOrEqual,
|
||||
});
|
||||
|
||||
/// Serialize model to json
|
||||
Map<String, dynamic> toJson() => _$PaginationParamsToJson(this);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'requests.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Map<String, dynamic> _$SortOptionToJson(SortOption instance) =>
|
||||
Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
|
||||
<String, dynamic>{
|
||||
'field': instance.field,
|
||||
'direction': instance.direction,
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/client.dart';
|
||||
import 'package:stream_chat/src/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/models/channel_state.dart';
|
||||
import 'package:stream_chat/src/models/device.dart';
|
||||
import 'package:stream_chat/src/models/event.dart';
|
||||
|
||||
import '../models/channel_model.dart';
|
||||
import '../models/channel_state.dart';
|
||||
import '../models/member.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/reaction.dart';
|
||||
import '../models/read.dart';
|
||||
import '../models/user.dart';
|
||||
import 'package:stream_chat/src/models/member.dart';
|
||||
import 'package:stream_chat/src/models/message.dart';
|
||||
import 'package:stream_chat/src/models/reaction.dart';
|
||||
import 'package:stream_chat/src/models/read.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
|
||||
part 'responses.g.dart';
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ class RetryPolicy {
|
||||
final bool Function(StreamChatClient client, int attempt, ApiError apiError)
|
||||
shouldRetry;
|
||||
|
||||
/// In the case that we want to retry a failed request the retryTimeout method is called to determine the timeout
|
||||
/// In the case that we want to retry a failed request the retryTimeout
|
||||
/// method is called to determine the timeout
|
||||
final Duration Function(
|
||||
StreamChatClient client, int attempt, ApiError apiError) retryTimeout;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:stream_chat/src/api/channel.dart';
|
||||
import 'package:stream_chat/src/api/retry_policy.dart';
|
||||
import 'package:stream_chat/src/event_type.dart';
|
||||
@@ -12,12 +12,6 @@ import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
/// The retry queue associated to a channel
|
||||
class RetryQueue {
|
||||
/// The channel of this queue
|
||||
final Channel channel;
|
||||
|
||||
/// The logger associated to this queue
|
||||
final Logger logger;
|
||||
|
||||
/// Instantiate a new RetryQueue object
|
||||
RetryQueue({
|
||||
@required this.channel,
|
||||
@@ -30,6 +24,12 @@ class RetryQueue {
|
||||
_listenFailedEvents();
|
||||
}
|
||||
|
||||
/// The channel of this queue
|
||||
final Channel channel;
|
||||
|
||||
/// The logger associated to this queue
|
||||
final Logger logger;
|
||||
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
|
||||
void _listenConnectionRecovered() {
|
||||
|
||||
@@ -2,28 +2,29 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import '../models/event.dart';
|
||||
import '../models/user.dart';
|
||||
import 'connection_status.dart';
|
||||
import 'web_socket_channel_stub.dart'
|
||||
import 'package:stream_chat/src/api/connection_status.dart';
|
||||
import 'package:stream_chat/src/api/web_socket_channel_stub.dart'
|
||||
if (dart.library.html) 'web_socket_channel_html.dart'
|
||||
if (dart.library.io) 'web_socket_channel_io.dart';
|
||||
import 'package:stream_chat/src/models/event.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
/// Typedef which exposes an [Event] as the only parameter.
|
||||
typedef EventHandler = void Function(Event);
|
||||
|
||||
/// Typedef used for connecting to a websocket. Method returns a [WebSocketChannel]
|
||||
/// and accepts a connection [url] and an optional [Iterable] of `protocols`.
|
||||
/// Typedef used for connecting to a websocket. Method returns a
|
||||
/// [WebSocketChannel] and accepts a connection [url] and an optional
|
||||
/// [Iterable] of `protocols`.
|
||||
typedef ConnectWebSocket = WebSocketChannel Function(String url,
|
||||
{Iterable<String> protocols});
|
||||
|
||||
// TODO: parse error even
|
||||
// TODO: if parsing an error into an event fails we should not hide the original error
|
||||
// TODO: if parsing an error into an event fails we should not hide the
|
||||
// TODO: original error
|
||||
/// A WebSocket connection that reconnects upon failure.
|
||||
class WebSocket {
|
||||
/// Creates a new websocket
|
||||
@@ -75,7 +76,8 @@ class WebSocket {
|
||||
/// WS connection payload
|
||||
final Map<String, dynamic> connectPayload;
|
||||
|
||||
/// Functions that will be called every time a new event is received from the connection
|
||||
/// Functions that will be called every time a new event is received from the
|
||||
/// connection
|
||||
final EventHandler handler;
|
||||
|
||||
/// A WS specific logger instance
|
||||
@@ -87,8 +89,9 @@ class WebSocket {
|
||||
final ConnectWebSocket connectFunc;
|
||||
|
||||
/// Interval of the reconnection monitor timer
|
||||
/// This checks that it received a new event in the last [reconnectionMonitorTimeout] seconds,
|
||||
/// otherwise it considers the connection unhealthy and reconnects the WS
|
||||
/// This checks that it received a new event in the last
|
||||
/// [reconnectionMonitorTimeout] seconds, otherwise it considers the
|
||||
/// connection unhealthy and reconnects the WS
|
||||
final int reconnectionMonitorInterval;
|
||||
|
||||
/// Interval of the health event sending timer
|
||||
@@ -96,7 +99,8 @@ class WebSocket {
|
||||
/// make the server aware that the client is still listening
|
||||
final int healthCheckInterval;
|
||||
|
||||
/// The timeout that uses the reconnection monitor timer to consider the connection unhealthy
|
||||
/// The timeout that uses the reconnection monitor timer to consider the
|
||||
/// connection unhealthy
|
||||
final int reconnectionMonitorTimeout;
|
||||
|
||||
final _connectionStatusController =
|
||||
@@ -121,9 +125,7 @@ class WebSocket {
|
||||
_connecting = false,
|
||||
_reconnecting = false;
|
||||
|
||||
Event _decodeEvent(String source) {
|
||||
return Event.fromJson(json.decode(source));
|
||||
}
|
||||
Event _decodeEvent(String source) => Event.fromJson(json.decode(source));
|
||||
|
||||
Completer<Event> _connectionCompleter = Completer<Event>();
|
||||
|
||||
@@ -166,8 +168,8 @@ class WebSocket {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'connection closed | closeCode: ${_channel.closeCode} | closedReason: ${_channel.closeReason}');
|
||||
logger.info('connection closed | closeCode: ${_channel.closeCode} | '
|
||||
'closedReason: ${_channel.closeReason}');
|
||||
|
||||
if (!_reconnecting) {
|
||||
_reconnect();
|
||||
@@ -200,8 +202,7 @@ class WebSocket {
|
||||
}
|
||||
|
||||
Future<void> _onConnectionError(error, [stacktrace]) async {
|
||||
logger.severe('error connecting');
|
||||
logger.severe(error);
|
||||
logger..severe('error connecting')..severe(error);
|
||||
if (stacktrace != null) {
|
||||
logger.severe(stacktrace);
|
||||
}
|
||||
@@ -219,21 +220,21 @@ class WebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
void _startReconnectionMonitor() {
|
||||
final reconnectionTimer = (_) {
|
||||
final now = DateTime.now();
|
||||
if (_lastEventAt != null &&
|
||||
now.difference(_lastEventAt).inSeconds > reconnectionMonitorTimeout) {
|
||||
_channel.sink.close();
|
||||
}
|
||||
};
|
||||
void _reconnectionTimer(_) {
|
||||
final now = DateTime.now();
|
||||
if (_lastEventAt != null &&
|
||||
now.difference(_lastEventAt).inSeconds > reconnectionMonitorTimeout) {
|
||||
_channel.sink.close();
|
||||
}
|
||||
}
|
||||
|
||||
void _startReconnectionMonitor() {
|
||||
_reconnectionMonitor = Timer.periodic(
|
||||
Duration(seconds: reconnectionMonitorInterval),
|
||||
reconnectionTimer,
|
||||
_reconnectionTimer,
|
||||
);
|
||||
|
||||
reconnectionTimer(_reconnectionMonitor);
|
||||
_reconnectionTimer(_reconnectionMonitor);
|
||||
}
|
||||
|
||||
void _reconnectTimer() async {
|
||||
@@ -283,20 +284,20 @@ class WebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
void _healthCheckTimer(_) {
|
||||
logger.info('sending health.check');
|
||||
_channel.sink.add("{'type': 'health.check'}");
|
||||
}
|
||||
|
||||
void _startHealthCheck() {
|
||||
logger.info('start health check monitor');
|
||||
|
||||
final healthCheckTimer = (_) {
|
||||
logger.info('sending health.check');
|
||||
_channel.sink.add("{'type': 'health.check'}");
|
||||
};
|
||||
|
||||
_healthCheck = Timer.periodic(
|
||||
Duration(seconds: healthCheckInterval),
|
||||
healthCheckTimer,
|
||||
_healthCheckTimer,
|
||||
);
|
||||
|
||||
healthCheckTimer(_healthCheck);
|
||||
_healthCheckTimer(_healthCheck);
|
||||
}
|
||||
|
||||
/// Disconnects the WS and releases eventual resources
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:stream_chat/src/api/responses.dart';
|
||||
import 'package:stream_chat/src/client.dart';
|
||||
import 'package:stream_chat/src/models/attachment_file.dart';
|
||||
import 'client.dart';
|
||||
import 'extensions/string_extension.dart';
|
||||
import 'package:stream_chat/src/extensions/string_extension.dart';
|
||||
|
||||
/// Class responsible for uploading images and files to a given channel
|
||||
abstract class AttachmentFileUploader {
|
||||
@@ -57,11 +57,11 @@ abstract class AttachmentFileUploader {
|
||||
|
||||
/// Stream's default implementation of [AttachmentFileUploader]
|
||||
class StreamAttachmentFileUploader implements AttachmentFileUploader {
|
||||
final StreamChatClient _client;
|
||||
|
||||
/// Creates a new [StreamAttachmentFileUploader] instance.
|
||||
const StreamAttachmentFileUploader(this._client);
|
||||
|
||||
final StreamChatClient _client;
|
||||
|
||||
@override
|
||||
Future<SendImageResponse> sendImage(
|
||||
AttachmentFile file,
|
||||
@@ -70,16 +70,28 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) async {
|
||||
final filename = file.path?.split('/')?.last;
|
||||
final filename = file.path?.split('/')?.last ?? file.name;
|
||||
final mimeType = filename.mimeType;
|
||||
|
||||
MultipartFile multiPartFile;
|
||||
if (file.path != null) {
|
||||
multiPartFile = await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
);
|
||||
} else if (file.bytes != null) {
|
||||
multiPartFile = MultipartFile.fromBytes(
|
||||
file.bytes,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await _client.post(
|
||||
'/channels/$channelType/$channelId/image',
|
||||
data: FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
),
|
||||
'file': multiPartFile,
|
||||
}),
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
@@ -95,16 +107,28 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) async {
|
||||
final filename = file.path?.split('/')?.last;
|
||||
final filename = file.path?.split('/')?.last ?? file.name;
|
||||
final mimeType = filename.mimeType;
|
||||
|
||||
MultipartFile multiPartFile;
|
||||
if (file.path != null) {
|
||||
multiPartFile = await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
);
|
||||
} else if (file.bytes != null) {
|
||||
multiPartFile = MultipartFile.fromBytes(
|
||||
file.bytes,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
);
|
||||
}
|
||||
|
||||
final response = await _client.post(
|
||||
'/channels/$channelType/$channelId/file',
|
||||
data: FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
),
|
||||
'file': multiPartFile,
|
||||
}),
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:stream_chat/src/extensions/map_extension.dart';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:pedantic/pedantic.dart' show unawaited;
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/src/api/channel.dart';
|
||||
import 'package:stream_chat/src/api/connection_status.dart';
|
||||
import 'package:stream_chat/src/api/requests.dart';
|
||||
import 'package:stream_chat/src/api/responses.dart';
|
||||
import 'package:stream_chat/src/api/retry_policy.dart';
|
||||
import 'package:stream_chat/src/api/websocket.dart';
|
||||
import 'package:stream_chat/src/attachment_file_uploader.dart';
|
||||
import 'package:stream_chat/src/db/chat_persistence_client.dart';
|
||||
import 'package:stream_chat/src/event_type.dart';
|
||||
import 'package:stream_chat/src/exceptions.dart';
|
||||
import 'package:stream_chat/src/models/attachment_file.dart';
|
||||
import 'package:stream_chat/src/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/models/channel_state.dart';
|
||||
import 'package:stream_chat/src/models/event.dart';
|
||||
import 'package:stream_chat/src/models/message.dart';
|
||||
import 'package:stream_chat/src/models/own_user.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
|
||||
import 'package:stream_chat/version.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'attachment_file_uploader.dart';
|
||||
import 'api/channel.dart';
|
||||
import 'api/connection_status.dart';
|
||||
import 'api/requests.dart';
|
||||
import 'api/responses.dart';
|
||||
import 'api/websocket.dart';
|
||||
import 'db/chat_persistence_client.dart';
|
||||
import 'exceptions.dart';
|
||||
import 'models/channel_state.dart';
|
||||
import 'models/event.dart';
|
||||
import 'models/message.dart';
|
||||
import 'models/user.dart';
|
||||
import 'extensions/map_extension.dart';
|
||||
|
||||
/// Handler function used for logging records. Function requires a single [LogRecord]
|
||||
/// as the only parameter.
|
||||
/// Handler function used for logging records. Function requires a single
|
||||
/// [LogRecord] as the only parameter.
|
||||
typedef LogHandlerFunction = void Function(LogRecord record);
|
||||
|
||||
/// Used for decoding [Map] data to a generic type `T`.
|
||||
@@ -62,7 +60,9 @@ extension on PushProvider {
|
||||
|
||||
/// The official Dart client for Stream Chat,
|
||||
/// a service for building chat applications.
|
||||
/// This library can be used on any Dart project and on both mobile and web apps with Flutter.
|
||||
/// This library can be used on any Dart project and on both mobile and web apps
|
||||
/// with Flutter.
|
||||
///
|
||||
/// You can sign up for a Stream account at https://getstream.io/chat/
|
||||
///
|
||||
/// The Chat client will manage API call, event handling and manage the
|
||||
@@ -73,7 +73,8 @@ extension on PushProvider {
|
||||
/// ```
|
||||
class StreamChatClient {
|
||||
/// Create a client instance with default options.
|
||||
/// You should only create the client once and re-use it across your application.
|
||||
/// You should only create the client once and re-use it across your
|
||||
/// application.
|
||||
StreamChatClient(
|
||||
this.apiKey, {
|
||||
this.tokenProvider,
|
||||
@@ -86,12 +87,14 @@ class StreamChatClient {
|
||||
RetryPolicy retryPolicy,
|
||||
this.attachmentFileUploader,
|
||||
}) {
|
||||
_retryPolicy ??= RetryPolicy(
|
||||
retryTimeout: (StreamChatClient client, int attempt, ApiError error) =>
|
||||
Duration(seconds: 1 * attempt),
|
||||
shouldRetry: (StreamChatClient client, int attempt, ApiError error) =>
|
||||
attempt < 5,
|
||||
);
|
||||
_retryPolicy = retryPolicy ??
|
||||
RetryPolicy(
|
||||
retryTimeout:
|
||||
(StreamChatClient client, int attempt, ApiError error) =>
|
||||
Duration(seconds: 1 * attempt),
|
||||
shouldRetry: (StreamChatClient client, int attempt, ApiError error) =>
|
||||
attempt < 5,
|
||||
);
|
||||
|
||||
attachmentFileUploader ??= StreamAttachmentFileUploader(this);
|
||||
|
||||
@@ -122,30 +125,39 @@ class StreamChatClient {
|
||||
/// This client state
|
||||
ClientState state;
|
||||
|
||||
/// By default the Chat client will write all messages with level Warn or Error to stdout.
|
||||
/// During development you might want to enable more logging information, you can change the default log level when constructing the client.
|
||||
/// By default the Chat client will write all messages with level Warn or
|
||||
/// Error to stdout.
|
||||
///
|
||||
/// During development you might want to enable more logging information,
|
||||
/// you can change the default log level when constructing the client.
|
||||
///
|
||||
/// ```dart
|
||||
/// final client = StreamChatClient("stream-chat-api-key", logLevel: Level.INFO);
|
||||
/// final client = StreamChatClient("stream-chat-api-key",
|
||||
/// logLevel: Level.INFO);
|
||||
/// ```
|
||||
final Level logLevel;
|
||||
|
||||
/// Client specific logger instance.
|
||||
/// Refer to the class [Logger] to learn more about the specific implementation.
|
||||
/// Refer to the class [Logger] to learn more about the specific
|
||||
/// implementation.
|
||||
final Logger logger = Logger.detached('📡');
|
||||
|
||||
/// A function that has a parameter of type [LogRecord].
|
||||
/// This is called on every new log record.
|
||||
/// By default the client will use the handler returned by [_getDefaultLogHandler].
|
||||
/// Setting it you can handle the log messages directly instead of have them written to stdout,
|
||||
/// this is very convenient if you use an error tracking tool or if you want to centralize your logs into one facility.
|
||||
/// By default the client will use the handler returned by
|
||||
/// [_getDefaultLogHandler].
|
||||
/// Setting it you can handle the log messages directly instead of have them
|
||||
/// written to stdout,
|
||||
/// this is very convenient if you use an error tracking tool or if you want
|
||||
/// to centralize your logs into one facility.
|
||||
///
|
||||
/// ```dart
|
||||
/// myLogHandlerFunction = (LogRecord record) {
|
||||
/// // do something with the record (ie. send it to Sentry or Fabric)
|
||||
/// }
|
||||
///
|
||||
/// final client = StreamChatClient("stream-chat-api-key", logHandlerFunction: myLogHandlerFunction);
|
||||
/// final client = StreamChatClient("stream-chat-api-key",
|
||||
/// logHandlerFunction: myLogHandlerFunction);
|
||||
///```
|
||||
LogHandlerFunction logHandlerFunction;
|
||||
|
||||
@@ -156,14 +168,18 @@ class StreamChatClient {
|
||||
/// Your project Stream Chat base url.
|
||||
final String baseURL;
|
||||
|
||||
/// A function in which you send a request to your own backend to get a Stream Chat API token.
|
||||
/// A function in which you send a request to your own backend to get a Stream
|
||||
/// Chat API token.
|
||||
///
|
||||
/// The token will be the return value of the function.
|
||||
/// It's used by the client to refresh the token once expired or to connect the user without a predefined token using [connectUserWithProvider].
|
||||
/// It's used by the client to refresh the token once expired or to connect
|
||||
/// the user without a predefined token using [connectUserWithProvider].
|
||||
final TokenProvider tokenProvider;
|
||||
|
||||
/// [Dio] httpClient
|
||||
/// It's be chosen because it's easy to use and supports interesting features out of the box
|
||||
/// (Interceptors, Global configuration, FormData, File downloading etc.)
|
||||
/// It's be chosen because it's easy to use and supports interesting features
|
||||
/// out of the box (Interceptors, Global configuration, FormData,
|
||||
/// File downloading etc.)
|
||||
@visibleForTesting
|
||||
Dio httpClient = Dio();
|
||||
|
||||
@@ -234,7 +250,7 @@ class StreamChatClient {
|
||||
(options.data is Map || options.data == null)) {
|
||||
options.data = {
|
||||
'connection_id': _connectionId,
|
||||
...(options.data ?? {}),
|
||||
...options.data ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -278,7 +294,7 @@ class StreamChatClient {
|
||||
await _disconnect();
|
||||
|
||||
final newToken = await tokenProvider(userId);
|
||||
await Future.delayed(Duration(seconds: 4));
|
||||
await Future.delayed(const Duration(seconds: 4));
|
||||
token = newToken;
|
||||
|
||||
httpClient.unlock();
|
||||
@@ -312,7 +328,10 @@ class StreamChatClient {
|
||||
};
|
||||
return (LogRecord record) {
|
||||
print(
|
||||
'(${record.time}) ${levelEmojiMapper[record.level.name] ?? record.level.name} ${record.loggerName} ${record.message}');
|
||||
'(${record.time}) '
|
||||
'${levelEmojiMapper[record.level.name] ?? record.level.name} '
|
||||
'${record.loggerName} ${record.message}',
|
||||
);
|
||||
if (record.stackTrace != null) {
|
||||
print(record.stackTrace);
|
||||
}
|
||||
@@ -321,11 +340,10 @@ class StreamChatClient {
|
||||
|
||||
Logger _detachedLogger(
|
||||
String name,
|
||||
) {
|
||||
return Logger.detached(name)
|
||||
..level = logLevel
|
||||
..onRecord.listen(logHandlerFunction ?? _getDefaultLogHandler());
|
||||
}
|
||||
) =>
|
||||
Logger.detached(name)
|
||||
..level = logLevel
|
||||
..onRecord.listen(logHandlerFunction ?? _getDefaultLogHandler());
|
||||
|
||||
void _setupLogger() {
|
||||
logger.level = logLevel;
|
||||
@@ -350,7 +368,7 @@ class StreamChatClient {
|
||||
Map<String, String> get _httpHeaders => {
|
||||
'Authorization': token,
|
||||
'stream-auth-type': _authType,
|
||||
'x-stream-client': _userAgent,
|
||||
'X-Stream-Client': _userAgent,
|
||||
'Content-Encoding': 'gzip',
|
||||
};
|
||||
|
||||
@@ -386,7 +404,8 @@ class StreamChatClient {
|
||||
/// Set the current user using the [tokenProvider] to fetch the token.
|
||||
/// It returns a [Future] that resolves when the connection is setup.
|
||||
@Deprecated(
|
||||
'Use `connectUserWithProvider` instead. Will be removed in Future releases')
|
||||
'Use `connectUserWithProvider` instead. Will be removed in Future releases',
|
||||
)
|
||||
Future<Event> setUserWithProvider(User user) => connectUserWithProvider(user);
|
||||
|
||||
/// Connects the current user using the [tokenProvider] to fetch the token.
|
||||
@@ -470,7 +489,7 @@ class StreamChatClient {
|
||||
'api_key': apiKey,
|
||||
'authorization': token,
|
||||
'stream-auth-type': _authType,
|
||||
'x-stream-client': _userAgent,
|
||||
'X-Stream-Client': _userAgent,
|
||||
},
|
||||
connectPayload: {
|
||||
'user_id': state.user.id,
|
||||
@@ -491,7 +510,8 @@ class StreamChatClient {
|
||||
|
||||
if (status == ConnectionStatus.connected &&
|
||||
state.channels?.isNotEmpty == true) {
|
||||
unawaited(queryChannelsOnline(filter: {
|
||||
// ignore: unawaited_futures
|
||||
queryChannelsOnline(filter: {
|
||||
'cid': {
|
||||
'\$in': state.channels.keys.toList(),
|
||||
},
|
||||
@@ -503,7 +523,7 @@ class StreamChatClient {
|
||||
online: true,
|
||||
));
|
||||
},
|
||||
));
|
||||
);
|
||||
} else {
|
||||
_synced = false;
|
||||
}
|
||||
@@ -521,6 +541,7 @@ class StreamChatClient {
|
||||
}).catchError((err, stacktrace) {
|
||||
logger.severe('error connecting ws', err, stacktrace);
|
||||
if (err is Map) {
|
||||
// ignore: only_throw_errors
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
@@ -558,13 +579,12 @@ class StreamChatClient {
|
||||
res.events.sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||
|
||||
res.events.forEach((element) {
|
||||
logger.fine('element.type: ${element.type}');
|
||||
logger.fine('element.message.text: ${element.message?.text}');
|
||||
logger
|
||||
..fine('element.type: ${element.type}')
|
||||
..fine('element.message.text: ${element.message?.text}');
|
||||
});
|
||||
|
||||
res.events.forEach((event) {
|
||||
handleEvent(event);
|
||||
});
|
||||
res.events.forEach(handleEvent);
|
||||
|
||||
await chatPersistenceClient?.updateLastSyncAt(DateTime.now());
|
||||
_synced = true;
|
||||
@@ -573,9 +593,7 @@ class StreamChatClient {
|
||||
}
|
||||
}
|
||||
|
||||
String _asMap(sort) {
|
||||
return sort?.map((s) => s.toJson().toString())?.join('');
|
||||
}
|
||||
String _asMap(sort) => sort?.map((s) => s.toJson().toString())?.join('');
|
||||
|
||||
final _queryChannelsStreams = <String, Future<List<Channel>>>{};
|
||||
|
||||
@@ -584,40 +602,41 @@ class StreamChatClient {
|
||||
Map<String, dynamic> filter,
|
||||
List<SortOption<ChannelModel>> sort,
|
||||
Map<String, dynamic> options,
|
||||
PaginationParams paginationParams = const PaginationParams(limit: 10),
|
||||
PaginationParams paginationParams = const PaginationParams(),
|
||||
int messageLimit,
|
||||
bool preferOffline = false,
|
||||
bool waitForConnect = true,
|
||||
}) async* {
|
||||
final hash = base64.encode(utf8.encode(
|
||||
'$filter${_asMap(sort)}$options${paginationParams?.toJson()}$messageLimit$preferOffline',
|
||||
'$filter${_asMap(sort)}$options${paginationParams?.toJson()}'
|
||||
'$messageLimit',
|
||||
));
|
||||
|
||||
if (_queryChannelsStreams.containsKey(hash)) {
|
||||
yield await _queryChannelsStreams[hash];
|
||||
} else {
|
||||
if (preferOffline) {
|
||||
final channels = await queryChannelsOffline(
|
||||
filter: filter,
|
||||
sort: sort,
|
||||
paginationParams: paginationParams,
|
||||
);
|
||||
if (channels.isNotEmpty) yield channels;
|
||||
}
|
||||
|
||||
final newQueryChannelsFuture = queryChannelsOnline(
|
||||
final channels = await queryChannelsOffline(
|
||||
filter: filter,
|
||||
sort: sort,
|
||||
options: options,
|
||||
paginationParams: paginationParams,
|
||||
messageLimit: messageLimit,
|
||||
).whenComplete(() {
|
||||
_queryChannelsStreams.remove(hash);
|
||||
});
|
||||
);
|
||||
if (channels.isNotEmpty) yield channels;
|
||||
|
||||
_queryChannelsStreams[hash] = newQueryChannelsFuture;
|
||||
if (wsConnectionStatus == ConnectionStatus.connected) {
|
||||
final newQueryChannelsFuture = queryChannelsOnline(
|
||||
filter: filter,
|
||||
sort: sort,
|
||||
options: options,
|
||||
paginationParams: paginationParams,
|
||||
messageLimit: messageLimit,
|
||||
waitForConnect: waitForConnect,
|
||||
).whenComplete(() {
|
||||
_queryChannelsStreams.remove(hash);
|
||||
});
|
||||
|
||||
yield await newQueryChannelsFuture;
|
||||
_queryChannelsStreams[hash] = newQueryChannelsFuture;
|
||||
|
||||
yield await newQueryChannelsFuture;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,7 +646,7 @@ class StreamChatClient {
|
||||
List<SortOption<ChannelModel>> sort,
|
||||
Map<String, dynamic> options,
|
||||
int messageLimit,
|
||||
PaginationParams paginationParams = const PaginationParams(limit: 10),
|
||||
PaginationParams paginationParams = const PaginationParams(),
|
||||
bool waitForConnect = true,
|
||||
}) async {
|
||||
if (waitForConnect) {
|
||||
@@ -650,7 +669,7 @@ class StreamChatClient {
|
||||
'presence': false,
|
||||
};
|
||||
|
||||
var payload = <String, dynamic>{
|
||||
final payload = <String, dynamic>{
|
||||
'filter_conditions': filter,
|
||||
'sort': sort,
|
||||
};
|
||||
@@ -682,9 +701,12 @@ class StreamChatClient {
|
||||
);
|
||||
|
||||
if ((res.channels ?? []).isEmpty && (paginationParams?.offset ?? 0) == 0) {
|
||||
logger.warning('''We could not find any channel for this query.
|
||||
logger.warning(
|
||||
'''
|
||||
We could not find any channel for this query.
|
||||
Please make sure to take a look at the Flutter tutorial: https://getstream.io/chat/flutter/tutorial
|
||||
If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart''');
|
||||
If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart''',
|
||||
);
|
||||
return <Channel>[];
|
||||
}
|
||||
|
||||
@@ -715,7 +737,7 @@ class StreamChatClient {
|
||||
Future<List<Channel>> queryChannelsOffline({
|
||||
@required Map<String, dynamic> filter,
|
||||
@required List<SortOption<ChannelModel>> sort,
|
||||
PaginationParams paginationParams = const PaginationParams(limit: 10),
|
||||
PaginationParams paginationParams = const PaginationParams(),
|
||||
}) async {
|
||||
final offlineChannels = await chatPersistenceClient?.getChannelStates(
|
||||
filter: filter,
|
||||
@@ -771,6 +793,7 @@ class StreamChatClient {
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
// ignore: only_throw_errors
|
||||
throw _parseError(error);
|
||||
}
|
||||
}
|
||||
@@ -791,6 +814,7 @@ class StreamChatClient {
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
// ignore: only_throw_errors
|
||||
throw _parseError(error);
|
||||
}
|
||||
}
|
||||
@@ -809,6 +833,7 @@ class StreamChatClient {
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
// ignore: only_throw_errors
|
||||
throw _parseError(error);
|
||||
}
|
||||
}
|
||||
@@ -827,6 +852,7 @@ class StreamChatClient {
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
// ignore: only_throw_errors
|
||||
throw _parseError(error);
|
||||
}
|
||||
}
|
||||
@@ -845,6 +871,7 @@ class StreamChatClient {
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
// ignore: only_throw_errors
|
||||
throw _parseError(error);
|
||||
}
|
||||
}
|
||||
@@ -864,8 +891,8 @@ class StreamChatClient {
|
||||
|
||||
String get _authType => _anonymous ? 'anonymous' : 'jwt';
|
||||
|
||||
String get _userAgent =>
|
||||
'stream-chat-dart-client-${CurrentPlatform.name}-${PACKAGE_VERSION.split('+')[0]}';
|
||||
String get _userAgent => 'stream-chat-dart-client-${CurrentPlatform.name}-'
|
||||
'${PACKAGE_VERSION.split('+')[0]}';
|
||||
|
||||
Map<String, String> get _commonQueryParams => {
|
||||
'user_id': state.user?.id,
|
||||
@@ -873,14 +900,15 @@ class StreamChatClient {
|
||||
'connection_id': _connectionId,
|
||||
};
|
||||
|
||||
/// Set the current user with an anonymous id, this triggers a connection to the API.
|
||||
/// It returns a [Future] that resolves when the connection is setup.
|
||||
/// Set the current user with an anonymous id, this triggers a connection to
|
||||
/// the API. It returns a [Future] that resolves when the connection is setup.
|
||||
@Deprecated(
|
||||
'Use `connectAnonymousUser` instead. Will be removed in Future releases')
|
||||
Future<Event> setAnonymousUser() => connectAnonymousUser();
|
||||
|
||||
/// Connects the current user with an anonymous id, this triggers a connection to the API.
|
||||
/// It returns a [Future] that resolves when the connection is setup.
|
||||
/// Connects the current user with an anonymous id, this triggers a connection
|
||||
/// to the API. It returns a [Future] that resolves when the connection is
|
||||
/// setup.
|
||||
Future<Event> connectAnonymousUser() async {
|
||||
if (_connectCompleter != null && !_connectCompleter.isCompleted) {
|
||||
logger.warning('Already connecting');
|
||||
@@ -923,14 +951,14 @@ class StreamChatClient {
|
||||
}
|
||||
|
||||
/// Closes the websocket connection and resets the client
|
||||
/// If [flushChatPersistence] is true the client deletes all offline user's data
|
||||
/// If [clearUser] is true the client unsets the current user
|
||||
/// If [flushChatPersistence] is true the client deletes all offline
|
||||
/// user's data. If [clearUser] is true the client unsets the current user
|
||||
Future<void> disconnect({
|
||||
bool flushChatPersistence = false,
|
||||
bool clearUser = false,
|
||||
}) async {
|
||||
logger.info(
|
||||
'Disconnecting flushOfflineStorage: $flushChatPersistence; clearUser: $clearUser');
|
||||
logger.info('Disconnecting flushOfflineStorage: $flushChatPersistence; '
|
||||
'clearUser: $clearUser');
|
||||
|
||||
await chatPersistenceClient?.disconnect(flush: flushChatPersistence);
|
||||
chatPersistenceClient = null;
|
||||
@@ -966,9 +994,7 @@ class StreamChatClient {
|
||||
final payload = <String, dynamic>{
|
||||
'filter_conditions': filter ?? {},
|
||||
'sort': sort,
|
||||
};
|
||||
|
||||
payload.addAll(defaultOptions);
|
||||
}..addAll(defaultOptions);
|
||||
|
||||
if (pagination != null) {
|
||||
payload.addAll(pagination.toJson());
|
||||
@@ -1016,7 +1042,7 @@ class StreamChatClient {
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}(), 'Check incoming params.');
|
||||
|
||||
final payload = {
|
||||
'filter_conditions': filters,
|
||||
@@ -1041,15 +1067,14 @@ class StreamChatClient {
|
||||
String channelType, {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) {
|
||||
return attachmentFileUploader.sendFile(
|
||||
file,
|
||||
channelId,
|
||||
channelType,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
attachmentFileUploader.sendFile(
|
||||
file,
|
||||
channelId,
|
||||
channelType,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
|
||||
/// Send a [image] to the [channelId] of type [channelType]
|
||||
Future<SendImageResponse> sendImage(
|
||||
@@ -1058,15 +1083,14 @@ class StreamChatClient {
|
||||
String channelType, {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) {
|
||||
return attachmentFileUploader.sendImage(
|
||||
image,
|
||||
channelId,
|
||||
channelType,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
attachmentFileUploader.sendImage(
|
||||
image,
|
||||
channelId,
|
||||
channelType,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
|
||||
/// Delete a file from this channel
|
||||
Future<EmptyResponse> deleteFile(
|
||||
@@ -1074,14 +1098,13 @@ class StreamChatClient {
|
||||
String channelId,
|
||||
String channelType, {
|
||||
CancelToken cancelToken,
|
||||
}) {
|
||||
return attachmentFileUploader.deleteFile(
|
||||
url,
|
||||
channelId,
|
||||
channelType,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
attachmentFileUploader.deleteFile(
|
||||
url,
|
||||
channelId,
|
||||
channelType,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
|
||||
/// Delete an image from this channel
|
||||
Future<EmptyResponse> deleteImage(
|
||||
@@ -1089,14 +1112,13 @@ class StreamChatClient {
|
||||
String channelId,
|
||||
String channelType, {
|
||||
CancelToken cancelToken,
|
||||
}) {
|
||||
return attachmentFileUploader.deleteImage(
|
||||
url,
|
||||
channelId,
|
||||
channelType,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
attachmentFileUploader.deleteImage(
|
||||
url,
|
||||
channelId,
|
||||
channelType,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
|
||||
/// Add a device for Push Notifications.
|
||||
Future<EmptyResponse> addDevice(String id, PushProvider pushProvider) async {
|
||||
@@ -1146,9 +1168,8 @@ class StreamChatClient {
|
||||
}
|
||||
|
||||
/// Update or Create the given user object.
|
||||
Future<UpdateUsersResponse> updateUser(User user) async {
|
||||
return updateUsers([user]);
|
||||
}
|
||||
Future<UpdateUsersResponse> updateUser(User user) async =>
|
||||
updateUsers([user]);
|
||||
|
||||
/// Batch update a list of users
|
||||
Future<UpdateUsersResponse> updateUsers(List<User> users) async {
|
||||
@@ -1197,23 +1218,21 @@ class StreamChatClient {
|
||||
Future<EmptyResponse> shadowBan(
|
||||
String targetID, [
|
||||
Map<String, dynamic> options = const {},
|
||||
]) async {
|
||||
return banUser(targetID, {
|
||||
'shadow': true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
]) async =>
|
||||
banUser(targetID, {
|
||||
'shadow': true,
|
||||
...options,
|
||||
});
|
||||
|
||||
/// Removes shadow ban from a user
|
||||
Future<EmptyResponse> removeShadowBan(
|
||||
String targetID, [
|
||||
Map<String, dynamic> options = const {},
|
||||
]) async {
|
||||
return unbanUser(targetID, {
|
||||
'shadow': true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
]) async =>
|
||||
unbanUser(targetID, {
|
||||
'shadow': true,
|
||||
...options,
|
||||
});
|
||||
|
||||
/// Mutes a user
|
||||
Future<EmptyResponse> muteUser(String targetID) async {
|
||||
@@ -1312,7 +1331,7 @@ class StreamChatClient {
|
||||
throw ArgumentError('Invalid timeout or Expiration date');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}(), 'Check whether time out is valid');
|
||||
|
||||
DateTime pinExpires;
|
||||
if (timeoutOrExpirationDate is DateTime) {
|
||||
@@ -1328,15 +1347,12 @@ class StreamChatClient {
|
||||
}
|
||||
|
||||
/// Unpins provided message
|
||||
Future<UpdateMessageResponse> unpinMessage(Message message) {
|
||||
return updateMessage(message.copyWith(pinned: false));
|
||||
}
|
||||
Future<UpdateMessageResponse> unpinMessage(Message message) =>
|
||||
updateMessage(message.copyWith(pinned: false));
|
||||
}
|
||||
|
||||
/// The class that handles the state of the channel listening to the events
|
||||
class ClientState {
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
|
||||
/// Creates a new instance listening to events and updating the state
|
||||
ClientState(this._client) {
|
||||
_subscriptions.addAll([
|
||||
@@ -1358,16 +1374,12 @@ class ClientState {
|
||||
.on()
|
||||
.where((event) => event.unreadChannels != null)
|
||||
.map((e) => e.unreadChannels)
|
||||
.listen((unreadChannels) {
|
||||
_unreadChannelsController.add(unreadChannels);
|
||||
}),
|
||||
.listen(_unreadChannelsController.add),
|
||||
_client
|
||||
.on()
|
||||
.where((event) => event.totalUnreadCount != null)
|
||||
.map((e) => e.totalUnreadCount)
|
||||
.listen((totalUnreadCount) {
|
||||
_totalUnreadCountController.add(totalUnreadCount);
|
||||
}),
|
||||
.listen(_totalUnreadCountController.add),
|
||||
]);
|
||||
|
||||
_listenChannelDeleted();
|
||||
@@ -1377,6 +1389,8 @@ class ClientState {
|
||||
_listenUserUpdated();
|
||||
}
|
||||
|
||||
final _subscriptions = <StreamSubscription>[];
|
||||
|
||||
/// Used internally for optimistic update of unread count
|
||||
set totalUnreadCount(int unreadCount) {
|
||||
_totalUnreadCountController?.add(unreadCount ?? 0);
|
||||
|
||||
@@ -101,18 +101,17 @@ abstract class ChatPersistenceClient {
|
||||
Future<void> updateChannelQueries(
|
||||
Map<String, dynamic> filter,
|
||||
List<String> cids,
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
bool clearQueryCache,
|
||||
);
|
||||
|
||||
/// Remove a message by [messageId]
|
||||
Future<void> deleteMessageById(String messageId) {
|
||||
return deleteMessageByIds([messageId]);
|
||||
}
|
||||
Future<void> deleteMessageById(String messageId) =>
|
||||
deleteMessageByIds([messageId]);
|
||||
|
||||
/// Remove a pinned message by [messageId]
|
||||
Future<void> deletePinnedMessageById(String messageId) {
|
||||
return deletePinnedMessageByIds([messageId]);
|
||||
}
|
||||
Future<void> deletePinnedMessageById(String messageId) =>
|
||||
deletePinnedMessageByIds([messageId]);
|
||||
|
||||
/// Remove a message by [messageIds]
|
||||
Future<void> deleteMessageByIds(List<String> messageIds);
|
||||
@@ -121,14 +120,11 @@ abstract class ChatPersistenceClient {
|
||||
Future<void> deletePinnedMessageByIds(List<String> messageIds);
|
||||
|
||||
/// Remove a message by channel [cid]
|
||||
Future<void> deleteMessageByCid(String cid) {
|
||||
return deleteMessageByCids([cid]);
|
||||
}
|
||||
Future<void> deleteMessageByCid(String cid) => deleteMessageByCids([cid]);
|
||||
|
||||
/// Remove a pinned message by channel [cid]
|
||||
Future<void> deletePinnedMessageByCid(String cid) {
|
||||
return deletePinnedMessageByCids([cid]);
|
||||
}
|
||||
Future<void> deletePinnedMessageByCid(String cid) async =>
|
||||
deletePinnedMessageByCids([cid]);
|
||||
|
||||
/// Remove a message by message [cids]
|
||||
Future<void> deleteMessageByCids(List<String> cids);
|
||||
@@ -175,9 +171,8 @@ abstract class ChatPersistenceClient {
|
||||
Future<void> deleteMembersByCids(List<String> cids);
|
||||
|
||||
/// Update the channel state data using [channelState]
|
||||
Future<void> updateChannelState(ChannelState channelState) {
|
||||
return updateChannelStates([channelState]);
|
||||
}
|
||||
Future<void> updateChannelState(ChannelState channelState) =>
|
||||
updateChannelStates([channelState]);
|
||||
|
||||
/// Update list of channel states
|
||||
Future<void> updateChannelStates(List<ChannelState> channelStates) async {
|
||||
@@ -195,31 +190,31 @@ abstract class ChatPersistenceClient {
|
||||
deleteMembers,
|
||||
]);
|
||||
|
||||
final channels = channelStates.map((it) {
|
||||
return it.channel;
|
||||
}).where((it) => it != null);
|
||||
final channels =
|
||||
channelStates.map((it) => it.channel).where((it) => it != null);
|
||||
|
||||
final reactions = channelStates.expand((it) => it.messages).expand((it) {
|
||||
return [
|
||||
if (it.ownReactions != null)
|
||||
...it.ownReactions.where((r) => r.userId != null),
|
||||
if (it.latestReactions != null)
|
||||
...it.latestReactions.where((r) => r.userId != null)
|
||||
];
|
||||
}).where((it) => it != null);
|
||||
final reactions = channelStates
|
||||
.expand((it) => it.messages)
|
||||
.expand((it) => [
|
||||
if (it.ownReactions != null)
|
||||
...it.ownReactions.where((r) => r.userId != null),
|
||||
if (it.latestReactions != null)
|
||||
...it.latestReactions.where((r) => r.userId != null)
|
||||
])
|
||||
.where((it) => it != null);
|
||||
|
||||
final users = channelStates
|
||||
.map((cs) => [
|
||||
cs.channel?.createdBy,
|
||||
...cs.messages?.map((m) {
|
||||
return [
|
||||
m.user,
|
||||
if (m.latestReactions != null)
|
||||
...m.latestReactions.map((r) => r.user),
|
||||
if (m.ownReactions != null)
|
||||
...m.ownReactions.map((r) => r.user),
|
||||
];
|
||||
})?.expand((v) => v),
|
||||
...cs.messages
|
||||
?.map((m) => [
|
||||
m.user,
|
||||
if (m.latestReactions != null)
|
||||
...m.latestReactions.map((r) => r.user),
|
||||
if (m.ownReactions != null)
|
||||
...m.ownReactions.map((r) => r.user),
|
||||
])
|
||||
?.expand((v) => v),
|
||||
if (cs.read != null) ...cs.read.map((r) => r.user),
|
||||
if (cs.members != null) ...cs.members.map((m) => m.user),
|
||||
])
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Map of timeouts being debounced
|
||||
Map<Function, Timer> timeouts = {};
|
||||
|
||||
/// Runs a function avoiding calling it too many times in a [timeoutMS] window
|
||||
void debounce({
|
||||
@required Duration timeout,
|
||||
@required Function target,
|
||||
List positionalArguments,
|
||||
Map<Symbol, dynamic> namedArguments,
|
||||
}) {
|
||||
if (timeouts.containsKey(target)) {
|
||||
timeouts[target].cancel();
|
||||
}
|
||||
|
||||
final timer = Timer(timeout, () {
|
||||
Function.apply(
|
||||
target,
|
||||
positionalArguments,
|
||||
namedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
timeouts[target] = timer;
|
||||
}
|
||||
@@ -2,6 +2,13 @@ import 'dart:convert';
|
||||
|
||||
/// Exception related to api calls
|
||||
class ApiError extends Error {
|
||||
/// Creates a new ApiError instance using the response body and status code
|
||||
ApiError(this.body, this.status) : jsonData = _decode(body) {
|
||||
if (jsonData != null && jsonData.containsKey('code')) {
|
||||
_code = jsonData['code'];
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw body of the response
|
||||
final String body;
|
||||
|
||||
@@ -26,13 +33,6 @@ class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new ApiError instance using the response body and status code
|
||||
ApiError(this.body, this.status) : jsonData = _decode(body) {
|
||||
if (jsonData != null && jsonData.containsKey('code')) {
|
||||
_code = jsonData['code'];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
@@ -48,7 +48,6 @@ class ApiError extends Error {
|
||||
body.hashCode ^ jsonData.hashCode ^ status.hashCode ^ _code.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApiError{body: $body, jsonData: $jsonData, status: $status, code: $_code}';
|
||||
}
|
||||
String toString() => 'ApiError{body: $body, jsonData: $jsonData, '
|
||||
'status: $status, code: $_code}';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/// Useful extension functions for [Map]
|
||||
extension MapX on Map {
|
||||
/// Returns a new map with null keys or values removed
|
||||
Map<String, dynamic> get nullProtected {
|
||||
return {...this}..removeWhere((key, value) => key == null || value == null);
|
||||
}
|
||||
Map<String, dynamic> get nullProtected =>
|
||||
{...this}..removeWhere((key, value) => key == null || value == null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'dart:async' show Timer;
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// Useful rate limiter extensions for [Function] class.
|
||||
extension RateLimit on Function {
|
||||
/// Converts this into a [Debounce] function.
|
||||
Debounce debounced(
|
||||
Duration wait, {
|
||||
bool leading = false,
|
||||
bool trailing = true,
|
||||
Duration maxWait,
|
||||
}) =>
|
||||
Debounce(
|
||||
this,
|
||||
wait,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
maxWait: maxWait,
|
||||
);
|
||||
|
||||
/// Converts this into a [Throttle] function.
|
||||
Throttle throttled(
|
||||
Duration wait, {
|
||||
bool leading = true,
|
||||
bool trailing = true,
|
||||
}) =>
|
||||
Throttle(
|
||||
this,
|
||||
wait,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
||||
/// TopLevel lambda to create [Debounce] functions.
|
||||
Debounce debounce(
|
||||
Function func,
|
||||
Duration wait, {
|
||||
bool leading = false,
|
||||
bool trailing = true,
|
||||
Duration maxWait,
|
||||
}) =>
|
||||
Debounce(
|
||||
func,
|
||||
wait,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
maxWait: maxWait,
|
||||
);
|
||||
|
||||
/// TopLevel lambda to create [Throttle] functions.
|
||||
Throttle throttle(
|
||||
Function func,
|
||||
Duration wait, {
|
||||
bool leading = true,
|
||||
bool trailing = true,
|
||||
}) =>
|
||||
Throttle(
|
||||
func,
|
||||
wait,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
);
|
||||
|
||||
/// Creates a debounced function that delays invoking `func` until after `wait`
|
||||
/// milliseconds have elapsed since the last time the debounced function was
|
||||
/// invoked. The debounced function comes with a [Debounce.cancel] method to cancel
|
||||
/// delayed `func` invocations and a [Debounce.flush] method to immediately invoke them.
|
||||
/// Provide `leading` and/or `trailing` to indicate whether `func` should be
|
||||
/// invoked on the `leading` and/or `trailing` edge of the `wait` interval.
|
||||
/// The `func` is invoked with the last arguments provided to the [call]
|
||||
/// function. Subsequent calls to the debounced function return the result of
|
||||
/// the last `func` invocation.
|
||||
///
|
||||
/// **Note:** If `leading` and `trailing` options are `true`, `func` is
|
||||
/// invoked on the trailing edge of the timeout only if the debounced function
|
||||
/// is invoked more than once during the `wait` timeout.
|
||||
///
|
||||
/// If `wait` is [Duration.zero] and `leading` is `false`,
|
||||
/// `func` invocation is deferred until the next tick.
|
||||
///
|
||||
/// See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
|
||||
/// for details over the differences between [Debounce] and [Throttle].
|
||||
///
|
||||
/// Some examples:
|
||||
///
|
||||
/// Avoid calling costly network calls when user is typing something.
|
||||
/// ```dart
|
||||
/// void fetchData(String query) async {
|
||||
/// final data = api.getData(query);
|
||||
/// doSomethingWithTheData(data);
|
||||
/// }
|
||||
///
|
||||
/// final debouncedFetchData = Debounce(
|
||||
/// fetchData,
|
||||
/// const Duration(milliseconds: 350),
|
||||
/// );
|
||||
///
|
||||
/// void onSearchQueryChanged(query) {
|
||||
/// debouncedFetchData(query);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Cancel the trailing debounced invocation.
|
||||
/// ```dart
|
||||
/// void dispose() {
|
||||
/// debounced.cancel();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Check for pending invocations.
|
||||
/// ```dart
|
||||
/// final status = debounced.isPending ? "Pending..." : "Ready";
|
||||
/// ```
|
||||
class Debounce {
|
||||
/// Creates a new instance of [Debounce].
|
||||
Debounce(
|
||||
this._func,
|
||||
Duration wait, {
|
||||
bool leading = false,
|
||||
bool trailing = true,
|
||||
Duration maxWait,
|
||||
}) : _leading = leading,
|
||||
_trailing = trailing,
|
||||
_wait = wait?.inMilliseconds ?? 0,
|
||||
_maxing = maxWait != null {
|
||||
if (_maxing) {
|
||||
_maxWait = math.max(maxWait.inMilliseconds, _wait);
|
||||
}
|
||||
}
|
||||
|
||||
final Function _func;
|
||||
final bool _leading;
|
||||
final bool _trailing;
|
||||
final int _wait;
|
||||
final bool _maxing;
|
||||
|
||||
int _maxWait;
|
||||
List<Object> _lastArgs;
|
||||
Map<Symbol, Object> _lastNamedArgs;
|
||||
Timer _timer;
|
||||
int _lastCallTime;
|
||||
Object _result;
|
||||
int _lastInvokeTime = 0;
|
||||
|
||||
Object _invokeFunc(int time) {
|
||||
final args = _lastArgs;
|
||||
final namedArgs = _lastNamedArgs;
|
||||
_lastArgs = _lastNamedArgs = null;
|
||||
_lastInvokeTime = time;
|
||||
return _result = Function.apply(_func, args, namedArgs);
|
||||
}
|
||||
|
||||
Timer _startTimer(Function pendingFunc, int wait) =>
|
||||
Timer(Duration(milliseconds: wait), pendingFunc);
|
||||
|
||||
bool _shouldInvoke(int time) {
|
||||
final timeSinceLastCall = time - (_lastCallTime ?? double.nan);
|
||||
final timeSinceLastInvoke = time - _lastInvokeTime;
|
||||
|
||||
// Either this is the first call, activity has stopped and we're at the
|
||||
// trailing edge, the system time has gone backwards and we're treating
|
||||
// it as the trailing edge, or we've hit the `maxWait` limit.
|
||||
return _lastCallTime == null ||
|
||||
(timeSinceLastCall >= _wait) ||
|
||||
(timeSinceLastCall < 0) ||
|
||||
(_maxing && timeSinceLastInvoke >= _maxWait);
|
||||
}
|
||||
|
||||
Object _trailingEdge(int time) {
|
||||
_timer = null;
|
||||
|
||||
// Only invoke if we have `lastArgs` which means `func` has been
|
||||
// debounced at least once.
|
||||
if (_trailing && _lastArgs != null) {
|
||||
return _invokeFunc(time);
|
||||
}
|
||||
_lastArgs = _lastNamedArgs = null;
|
||||
return _result;
|
||||
}
|
||||
|
||||
int _remainingWait(int time) {
|
||||
final timeSinceLastCall = time - _lastCallTime;
|
||||
final timeSinceLastInvoke = time - _lastInvokeTime;
|
||||
final timeWaiting = _wait - timeSinceLastCall;
|
||||
|
||||
return _maxing
|
||||
? math.min(timeWaiting, _maxWait - timeSinceLastInvoke)
|
||||
: timeWaiting;
|
||||
}
|
||||
|
||||
void _timerExpired() {
|
||||
final time = DateTime.now().millisecondsSinceEpoch;
|
||||
if (_shouldInvoke(time)) {
|
||||
_trailingEdge(time);
|
||||
} else {
|
||||
// Restart the timer.
|
||||
_timer = _startTimer(_timerExpired, _remainingWait(time));
|
||||
}
|
||||
}
|
||||
|
||||
Object _leadingEdge(int time) {
|
||||
// Reset any `maxWait` timer.
|
||||
_lastInvokeTime = time;
|
||||
// Start the timer for the trailing edge.
|
||||
_timer = _startTimer(_timerExpired, _wait);
|
||||
// Invoke the leading edge.
|
||||
return _leading ? _invokeFunc(time) : _result;
|
||||
}
|
||||
|
||||
/// Cancels all the remaining delayed functions.
|
||||
void cancel() {
|
||||
_timer?.cancel();
|
||||
_lastInvokeTime = 0;
|
||||
_lastArgs = _lastNamedArgs = _lastCallTime = _timer = null;
|
||||
}
|
||||
|
||||
/// Immediately invokes all the remaining delayed functions.
|
||||
Object flush() {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
return _timer == null ? _result : _trailingEdge(now);
|
||||
}
|
||||
|
||||
/// True if there are functions remaining to get invoked.
|
||||
bool get isPending => _timer != null;
|
||||
|
||||
/// Calls/invokes this class like a function.
|
||||
/// Pass [args] and [namedArgs] to be used while invoking [_func].
|
||||
Object call(
|
||||
List<dynamic> args, {
|
||||
Map<Symbol, dynamic> namedArgs,
|
||||
}) {
|
||||
final time = DateTime.now().millisecondsSinceEpoch;
|
||||
final isInvoking = _shouldInvoke(time);
|
||||
|
||||
_lastArgs = args;
|
||||
_lastNamedArgs = namedArgs;
|
||||
_lastCallTime = time;
|
||||
|
||||
if (isInvoking) {
|
||||
if (_timer == null) {
|
||||
return _leadingEdge(_lastCallTime);
|
||||
}
|
||||
if (_maxing) {
|
||||
// Handle invocations in a tight loop.
|
||||
_timer = _startTimer(_timerExpired, _wait);
|
||||
return _invokeFunc(_lastCallTime);
|
||||
}
|
||||
}
|
||||
_timer ??= _startTimer(_timerExpired, _wait);
|
||||
return _result;
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a throttled function that only invokes `func` at most once per
|
||||
/// every `wait` milliseconds. The throttled function comes with a [Throttle.cancel]
|
||||
/// method to cancel delayed `func` invocations and a [Throttle.flush] method to
|
||||
/// immediately invoke them. Provide `leading` and/or `trailing` to indicate
|
||||
/// whether `func` should be invoked on the `leading` and/or `trailing` edge of the `wait` timeout.
|
||||
/// The `func` is invoked with the last arguments provided to the
|
||||
/// throttled function. Subsequent calls to the throttled function return the
|
||||
/// result of the last `func` invocation.
|
||||
///
|
||||
/// **Note:** If `leading` and `trailing` options are `true`, `func` is
|
||||
/// invoked on the trailing edge of the timeout only if the throttled function
|
||||
/// is invoked more than once during the `wait` timeout.
|
||||
///
|
||||
/// If `wait` is [Duration.zero] and `leading` is `false`, `func` invocation is deferred
|
||||
/// until the next tick.
|
||||
///
|
||||
/// See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
|
||||
/// for details over the differences between [Throttle] and [Debounce].
|
||||
///
|
||||
/// Some examples:
|
||||
///
|
||||
/// Avoid excessively rebuilding UI progress while uploading data to server.
|
||||
/// ```dart
|
||||
/// void updateUI(Data data) {
|
||||
/// updateProgress(data);
|
||||
/// }
|
||||
///
|
||||
/// final throttledUpdateUI = Throttle(
|
||||
/// updateUI,
|
||||
/// const Duration(milliseconds: 350),
|
||||
/// );
|
||||
///
|
||||
/// void onUploadProgressChanged(progress) {
|
||||
/// throttledUpdateUI(progress);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Cancel the trailing throttled invocation.
|
||||
/// ```dart
|
||||
/// void dispose() {
|
||||
/// throttled.cancel();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Check for pending invocations.
|
||||
/// ```dart
|
||||
/// final status = throttled.isPending ? "Pending..." : "Ready";
|
||||
/// ```
|
||||
class Throttle {
|
||||
/// Creates a new instance of [Throttle]
|
||||
Throttle(
|
||||
Function func,
|
||||
Duration wait, {
|
||||
bool leading = true,
|
||||
bool trailing = true,
|
||||
}) : _debounce = Debounce(
|
||||
func,
|
||||
wait,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
maxWait: wait,
|
||||
);
|
||||
|
||||
final Debounce _debounce;
|
||||
|
||||
/// Cancels all the remaining delayed functions.
|
||||
void cancel() => _debounce.cancel();
|
||||
|
||||
/// Immediately invokes all the remaining delayed functions.
|
||||
Object flush() => _debounce.flush();
|
||||
|
||||
/// True if there are functions remaining to get invoked.
|
||||
bool get isPending => _debounce.isPending;
|
||||
|
||||
/// Calls/invokes this class like a function.
|
||||
/// Pass [args] and [namedArgs] to be used while invoking `func`.
|
||||
Object call(List<dynamic> args, {Map<Symbol, dynamic> namedArgs}) =>
|
||||
_debounce.call(args, namedArgs: namedArgs);
|
||||
}
|
||||
@@ -5,6 +5,12 @@ part 'action.g.dart';
|
||||
/// The class that contains the information about an action
|
||||
@JsonSerializable()
|
||||
class Action {
|
||||
/// Constructor used for json serialization
|
||||
Action({this.name, this.style, this.text, this.type, this.value});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Action.fromJson(Map<String, dynamic> json) => _$ActionFromJson(json);
|
||||
|
||||
/// The name of the action
|
||||
final String name;
|
||||
|
||||
@@ -20,12 +26,6 @@ class Action {
|
||||
/// The value of the action
|
||||
final String value;
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Action({this.name, this.style, this.text, this.type, this.value});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Action.fromJson(Map<String, dynamic> json) => _$ActionFromJson(json);
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => _$ActionToJson(this);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,61 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/action.dart';
|
||||
import 'package:stream_chat/src/models/attachment_file.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'action.dart';
|
||||
import 'serialization.dart';
|
||||
|
||||
part 'attachment.g.dart';
|
||||
|
||||
/// The class that contains the information about an attachment
|
||||
@JsonSerializable(includeIfNull: false)
|
||||
class Attachment {
|
||||
///The attachment type based on the URL resource. This can be: audio, image or video
|
||||
/// Constructor used for json serialization
|
||||
Attachment({
|
||||
String id,
|
||||
this.type,
|
||||
this.titleLink,
|
||||
String title,
|
||||
this.thumbUrl,
|
||||
this.text,
|
||||
this.pretext,
|
||||
this.ogScrapeUrl,
|
||||
this.imageUrl,
|
||||
this.footerIcon,
|
||||
this.footer,
|
||||
this.fields,
|
||||
this.fallback,
|
||||
this.color,
|
||||
this.authorName,
|
||||
this.authorLink,
|
||||
this.authorIcon,
|
||||
this.assetUrl,
|
||||
this.actions,
|
||||
this.extraData,
|
||||
this.file,
|
||||
UploadState uploadState,
|
||||
}) : id = id ?? Uuid().v4(),
|
||||
title = title ?? file?.name,
|
||||
localUri = file?.path != null ? Uri.parse(file.path) : null {
|
||||
this.uploadState = uploadState ??
|
||||
((assetUrl != null || imageUrl != null)
|
||||
? const UploadState.success()
|
||||
: const UploadState.preparing());
|
||||
}
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Attachment.fromJson(Map<String, dynamic> json) =>
|
||||
_$AttachmentFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// Create a new instance from a db data
|
||||
factory Attachment.fromData(Map<String, dynamic> json) =>
|
||||
_$AttachmentFromJson(Serialization.moveToExtraDataFromRoot(
|
||||
json, topLevelFields + dbSpecificTopLevelFields));
|
||||
|
||||
///The attachment type based on the URL resource. This can be: audio,
|
||||
///image or video
|
||||
final String type;
|
||||
|
||||
///The link to which the attachment message points to.
|
||||
@@ -21,10 +64,12 @@ class Attachment {
|
||||
/// The attachment title
|
||||
final String title;
|
||||
|
||||
/// The URL to the attached file thumbnail. You can use this to represent the attached link.
|
||||
/// The URL to the attached file thumbnail. You can use this to represent the
|
||||
/// attached link.
|
||||
final String thumbUrl;
|
||||
|
||||
/// The attachment text. It will be displayed in the channel next to the original message.
|
||||
/// The attachment text. It will be displayed in the channel next to the
|
||||
/// original message.
|
||||
final String text;
|
||||
|
||||
/// Optional text that appears above the attachment block
|
||||
@@ -33,7 +78,8 @@ class Attachment {
|
||||
/// The original URL that was used to scrape this attachment.
|
||||
final String ogScrapeUrl;
|
||||
|
||||
/// The URL to the attached image. This is present for URL pointing to an image article (eg. Unsplash)
|
||||
/// The URL to the attached image. This is present for URL pointing to an
|
||||
/// image article (eg. Unsplash)
|
||||
final String imageUrl;
|
||||
final String footerIcon;
|
||||
final String footer;
|
||||
@@ -100,56 +146,11 @@ class Attachment {
|
||||
'file',
|
||||
];
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Attachment({
|
||||
String id,
|
||||
this.type,
|
||||
this.titleLink,
|
||||
String title,
|
||||
this.thumbUrl,
|
||||
this.text,
|
||||
this.pretext,
|
||||
this.ogScrapeUrl,
|
||||
this.imageUrl,
|
||||
this.footerIcon,
|
||||
this.footer,
|
||||
this.fields,
|
||||
this.fallback,
|
||||
this.color,
|
||||
this.authorName,
|
||||
this.authorLink,
|
||||
this.authorIcon,
|
||||
this.assetUrl,
|
||||
this.actions,
|
||||
this.extraData,
|
||||
this.file,
|
||||
UploadState uploadState,
|
||||
}) : id = id ?? Uuid().v4(),
|
||||
title = title ?? file?.name,
|
||||
localUri = file?.path != null ? Uri.parse(file.path) : null {
|
||||
this.uploadState = uploadState ??
|
||||
((assetUrl != null || imageUrl != null)
|
||||
? UploadState.success()
|
||||
: UploadState.preparing());
|
||||
}
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Attachment.fromJson(Map<String, dynamic> json) {
|
||||
return _$AttachmentFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
}
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
_$AttachmentToJson(this), topLevelFields)
|
||||
..removeWhere((key, value) => dbSpecificTopLevelFields.contains(key));
|
||||
|
||||
/// Create a new instance from a db data
|
||||
factory Attachment.fromData(Map<String, dynamic> json) {
|
||||
return _$AttachmentFromJson(Serialization.moveToExtraDataFromRoot(
|
||||
json, topLevelFields + dbSpecificTopLevelFields));
|
||||
}
|
||||
|
||||
/// Serialize to db data
|
||||
Map<String, dynamic> toData() => Serialization.moveFromExtraDataToRoot(
|
||||
_$AttachmentToJson(this), topLevelFields + dbSpecificTopLevelFields);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'attachment_file.freezed.dart';
|
||||
|
||||
part 'attachment_file.g.dart';
|
||||
|
||||
/// Union class to hold various [UploadState] of a attachment.
|
||||
@@ -57,8 +56,12 @@ class AttachmentFile {
|
||||
this.size,
|
||||
});
|
||||
|
||||
/// The absolute path for a cached copy of this file. It can be used to create a
|
||||
/// file instance with a descriptor for the given path.
|
||||
/// Create a new instance from a json
|
||||
factory AttachmentFile.fromJson(Map<String, dynamic> json) =>
|
||||
_$AttachmentFileFromJson(json);
|
||||
|
||||
/// The absolute path for a cached copy of this file. It can be used to
|
||||
/// create a file instance with a descriptor for the given path.
|
||||
/// ```
|
||||
/// final File myFile = File(platformFile.path);
|
||||
/// ```
|
||||
@@ -67,8 +70,8 @@ class AttachmentFile {
|
||||
/// File name including its extension.
|
||||
final String name;
|
||||
|
||||
/// Byte data for this file. Particularly useful if you want to manipulate its data
|
||||
/// or easily upload to somewhere else.
|
||||
/// Byte data for this file. Particularly useful if you want to manipulate
|
||||
/// its data or easily upload to somewhere else.
|
||||
@JsonKey(toJson: _toString, fromJson: _fromString)
|
||||
final Uint8List bytes;
|
||||
|
||||
@@ -78,11 +81,6 @@ class AttachmentFile {
|
||||
/// File extension for this file.
|
||||
String get extension => name?.split('.')?.last;
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory AttachmentFile.fromJson(Map<String, dynamic> json) {
|
||||
return _$AttachmentFileFromJson(json);
|
||||
}
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => _$AttachmentFileToJson(this);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,34 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'command.dart';
|
||||
|
||||
import 'package:stream_chat/src/models/command.dart';
|
||||
part 'channel_config.g.dart';
|
||||
|
||||
/// The class that contains the information about the configuration of a channel
|
||||
@JsonSerializable()
|
||||
class ChannelConfig {
|
||||
/// Constructor used for json serialization
|
||||
ChannelConfig({
|
||||
this.automod,
|
||||
this.commands,
|
||||
this.connectEvents,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.maxMessageLength,
|
||||
this.messageRetention,
|
||||
this.mutes,
|
||||
this.name,
|
||||
this.reactions,
|
||||
this.readEvents,
|
||||
this.replies,
|
||||
this.search,
|
||||
this.typingEvents,
|
||||
this.uploads,
|
||||
this.urlEnrichment,
|
||||
});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory ChannelConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$ChannelConfigFromJson(json);
|
||||
|
||||
/// Moderation configuration
|
||||
final String automod;
|
||||
|
||||
@@ -55,30 +77,6 @@ class ChannelConfig {
|
||||
/// True if urls appears as attachments
|
||||
final bool urlEnrichment;
|
||||
|
||||
/// Constructor used for json serialization
|
||||
ChannelConfig({
|
||||
this.automod,
|
||||
this.commands,
|
||||
this.connectEvents,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.maxMessageLength,
|
||||
this.messageRetention,
|
||||
this.mutes,
|
||||
this.name,
|
||||
this.reactions,
|
||||
this.readEvents,
|
||||
this.replies,
|
||||
this.search,
|
||||
this.typingEvents,
|
||||
this.uploads,
|
||||
this.urlEnrichment,
|
||||
});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory ChannelConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$ChannelConfigFromJson(json);
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => _$ChannelConfigToJson(this);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'channel_config.dart';
|
||||
import 'serialization.dart';
|
||||
import 'user.dart';
|
||||
import 'package:stream_chat/src/models/channel_config.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
|
||||
part 'channel_model.g.dart';
|
||||
|
||||
/// The class that contains the information about a channel
|
||||
@JsonSerializable()
|
||||
class ChannelModel {
|
||||
/// Constructor used for json serialization
|
||||
ChannelModel({
|
||||
this.id,
|
||||
this.type,
|
||||
this.cid,
|
||||
this.config,
|
||||
this.createdBy,
|
||||
this.frozen,
|
||||
this.lastMessageAt,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.memberCount,
|
||||
this.extraData,
|
||||
this.team,
|
||||
});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory ChannelModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ChannelModelFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// The id of this channel
|
||||
final String id;
|
||||
|
||||
@@ -76,40 +97,15 @@ class ChannelModel {
|
||||
'team',
|
||||
];
|
||||
|
||||
/// Constructor used for json serialization
|
||||
ChannelModel({
|
||||
this.id,
|
||||
this.type,
|
||||
this.cid,
|
||||
this.config,
|
||||
this.createdBy,
|
||||
this.frozen,
|
||||
this.lastMessageAt,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.memberCount,
|
||||
this.extraData,
|
||||
this.team,
|
||||
});
|
||||
|
||||
/// Shortcut for channel name
|
||||
String get name =>
|
||||
extraData?.containsKey('name') == true ? extraData['name'] : cid;
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory ChannelModel.fromJson(Map<String, dynamic> json) {
|
||||
return _$ChannelModelFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
}
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() {
|
||||
return Serialization.moveFromExtraDataToRoot(
|
||||
_$ChannelModelToJson(this),
|
||||
topLevelFields,
|
||||
);
|
||||
}
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
_$ChannelModelToJson(this),
|
||||
topLevelFields,
|
||||
);
|
||||
|
||||
/// Creates a copy of [ChannelModel] with specified attributes overridden.
|
||||
ChannelModel copyWith({
|
||||
@@ -143,8 +139,8 @@ class ChannelModel {
|
||||
team: team ?? this.team,
|
||||
);
|
||||
|
||||
/// Returns a new [ChannelModel] that is a combination of this channelModel and the given
|
||||
/// [other] channelModel.
|
||||
/// Returns a new [ChannelModel] that is a combination of this channelModel
|
||||
/// and the given [other] channelModel.
|
||||
ChannelModel merge(ChannelModel other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../models/read.dart';
|
||||
import '../models/user.dart';
|
||||
import 'channel_model.dart';
|
||||
import 'member.dart';
|
||||
import 'message.dart';
|
||||
import 'package:stream_chat/src/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/models/member.dart';
|
||||
import 'package:stream_chat/src/models/message.dart';
|
||||
import 'package:stream_chat/src/models/read.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
|
||||
part 'channel_state.g.dart';
|
||||
|
||||
/// The class that contains the information about a channel
|
||||
@JsonSerializable()
|
||||
class ChannelState {
|
||||
/// Constructor used for json serialization
|
||||
ChannelState({
|
||||
this.channel,
|
||||
this.messages = const [],
|
||||
this.members = const [],
|
||||
this.pinnedMessages = const [],
|
||||
this.watcherCount,
|
||||
this.watchers = const [],
|
||||
this.read = const [],
|
||||
});
|
||||
|
||||
/// The channel to which this state belongs
|
||||
final ChannelModel channel;
|
||||
|
||||
@@ -32,17 +42,6 @@ class ChannelState {
|
||||
/// The list of channel reads
|
||||
final List<Read> read;
|
||||
|
||||
/// Constructor used for json serialization
|
||||
ChannelState({
|
||||
this.channel,
|
||||
this.messages = const [],
|
||||
this.members = const [],
|
||||
this.pinnedMessages = const [],
|
||||
this.watcherCount,
|
||||
this.watchers = const [],
|
||||
this.read = const [],
|
||||
});
|
||||
|
||||
/// Create a new instance from a json
|
||||
static ChannelState fromJson(Map<String, dynamic> json) =>
|
||||
_$ChannelStateFromJson(json);
|
||||
|
||||
@@ -5,15 +5,6 @@ part 'command.g.dart';
|
||||
/// The class that contains the information about a command
|
||||
@JsonSerializable()
|
||||
class Command {
|
||||
/// The name of the command
|
||||
final String name;
|
||||
|
||||
/// The description explaining the command
|
||||
final String description;
|
||||
|
||||
/// The arguments of the command
|
||||
final String args;
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Command({
|
||||
this.name,
|
||||
@@ -25,6 +16,15 @@ class Command {
|
||||
factory Command.fromJson(Map<String, dynamic> json) =>
|
||||
_$CommandFromJson(json);
|
||||
|
||||
/// The name of the command
|
||||
final String name;
|
||||
|
||||
/// The description explaining the command
|
||||
final String description;
|
||||
|
||||
/// The arguments of the command
|
||||
final String args;
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => _$CommandToJson(this);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,6 @@ part 'device.g.dart';
|
||||
/// The class that contains the information about a device
|
||||
@JsonSerializable()
|
||||
class Device {
|
||||
/// The id of the device
|
||||
final String id;
|
||||
|
||||
/// The notification push provider
|
||||
final String pushProvider;
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Device({
|
||||
this.id,
|
||||
@@ -20,6 +14,12 @@ class Device {
|
||||
/// Create a new instance from a json
|
||||
factory Device.fromJson(Map<String, dynamic> json) => _$DeviceFromJson(json);
|
||||
|
||||
/// The id of the device
|
||||
final String id;
|
||||
|
||||
/// The notification push provider
|
||||
final String pushProvider;
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => _$DeviceToJson(this);
|
||||
}
|
||||
|
||||
@@ -4,17 +4,40 @@ import 'package:stream_chat/src/models/message.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
import '../event_type.dart';
|
||||
import 'member.dart';
|
||||
import 'own_user.dart';
|
||||
import 'reaction.dart';
|
||||
import 'user.dart';
|
||||
|
||||
part 'event.g.dart';
|
||||
|
||||
/// The class that contains the information about an event
|
||||
@JsonSerializable()
|
||||
class Event {
|
||||
/// Constructor used for json serialization
|
||||
Event({
|
||||
this.type,
|
||||
this.cid,
|
||||
this.connectionId,
|
||||
this.createdAt,
|
||||
this.me,
|
||||
this.user,
|
||||
this.message,
|
||||
this.totalUnreadCount,
|
||||
this.unreadChannels,
|
||||
this.reaction,
|
||||
this.online,
|
||||
this.channel,
|
||||
this.member,
|
||||
this.channelId,
|
||||
this.channelType,
|
||||
this.parentId,
|
||||
this.extraData,
|
||||
}) : isLocal = true;
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Event.fromJson(Map<String, dynamic> json) =>
|
||||
_$EventFromJson(Serialization.moveToExtraDataFromRoot(
|
||||
json,
|
||||
topLevelFields,
|
||||
))
|
||||
..isLocal = false;
|
||||
|
||||
/// The type of the event
|
||||
/// [EventType] contains some predefined constant types
|
||||
final String type;
|
||||
@@ -71,27 +94,6 @@ class Event {
|
||||
@JsonKey(includeIfNull: false)
|
||||
final Map<String, dynamic> extraData;
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Event({
|
||||
this.type,
|
||||
this.cid,
|
||||
this.connectionId,
|
||||
this.createdAt,
|
||||
this.me,
|
||||
this.user,
|
||||
this.message,
|
||||
this.totalUnreadCount,
|
||||
this.unreadChannels,
|
||||
this.reaction,
|
||||
this.online,
|
||||
this.channel,
|
||||
this.member,
|
||||
this.channelId,
|
||||
this.channelType,
|
||||
this.parentId,
|
||||
this.extraData,
|
||||
}) : isLocal = true;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
static final topLevelFields = [
|
||||
@@ -114,15 +116,6 @@ class Event {
|
||||
'is_local',
|
||||
];
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Event.fromJson(Map<String, dynamic> json) {
|
||||
return _$EventFromJson(Serialization.moveToExtraDataFromRoot(
|
||||
json,
|
||||
topLevelFields,
|
||||
))
|
||||
..isLocal = false;
|
||||
}
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
_$EventToJson(this),
|
||||
@@ -133,16 +126,6 @@ class Event {
|
||||
/// The channel embedded in the event object
|
||||
@JsonSerializable()
|
||||
class EventChannel extends ChannelModel {
|
||||
/// A paginated list of channel members
|
||||
final List<Member> members;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
static final topLevelFields = [
|
||||
'members',
|
||||
...ChannelModel.topLevelFields,
|
||||
];
|
||||
|
||||
/// Constructor used for json serialization
|
||||
EventChannel({
|
||||
this.members,
|
||||
@@ -174,12 +157,21 @@ class EventChannel extends ChannelModel {
|
||||
);
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory EventChannel.fromJson(Map<String, dynamic> json) {
|
||||
return _$EventChannelFromJson(Serialization.moveToExtraDataFromRoot(
|
||||
json,
|
||||
topLevelFields,
|
||||
));
|
||||
}
|
||||
factory EventChannel.fromJson(Map<String, dynamic> json) =>
|
||||
_$EventChannelFromJson(Serialization.moveToExtraDataFromRoot(
|
||||
json,
|
||||
topLevelFields,
|
||||
));
|
||||
|
||||
/// A paginated list of channel members
|
||||
final List<Member> members;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
static final topLevelFields = [
|
||||
'members',
|
||||
...ChannelModel.topLevelFields,
|
||||
];
|
||||
|
||||
/// Serialize to json
|
||||
@override
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../models/user.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
|
||||
part 'member.g.dart';
|
||||
|
||||
/// The class that contains the information about the user membership in a channel
|
||||
/// The class that contains the information about the user membership
|
||||
/// in a channel
|
||||
@JsonSerializable()
|
||||
class Member {
|
||||
/// Constructor used for json serialization
|
||||
Member({
|
||||
this.user,
|
||||
this.inviteAcceptedAt,
|
||||
this.inviteRejectedAt,
|
||||
this.invited,
|
||||
this.role,
|
||||
this.userId,
|
||||
this.isModerator,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.banned,
|
||||
this.shadowBanned,
|
||||
});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Member.fromJson(Map<String, dynamic> json) {
|
||||
final member = _$MemberFromJson(json);
|
||||
return member.copyWith(
|
||||
userId: member.user?.id,
|
||||
);
|
||||
}
|
||||
|
||||
/// The interested user
|
||||
final User user;
|
||||
|
||||
@@ -40,29 +63,6 @@ class Member {
|
||||
/// The last date of update
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Member({
|
||||
this.user,
|
||||
this.inviteAcceptedAt,
|
||||
this.inviteRejectedAt,
|
||||
this.invited,
|
||||
this.role,
|
||||
this.userId,
|
||||
this.isModerator,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.banned,
|
||||
this.shadowBanned,
|
||||
});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Member.fromJson(Map<String, dynamic> json) {
|
||||
final member = _$MemberFromJson(json);
|
||||
return member.copyWith(
|
||||
userId: member.user?.id,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a copy of [Member] with specified attributes overridden.
|
||||
Member copyWith({
|
||||
User user,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/attachment.dart';
|
||||
import 'package:stream_chat/src/models/reaction.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'attachment.dart';
|
||||
import 'reaction.dart';
|
||||
import 'serialization.dart';
|
||||
import 'user.dart';
|
||||
|
||||
part 'message.g.dart';
|
||||
|
||||
class _PinExpires {
|
||||
@@ -29,9 +28,11 @@ enum MessageSendingStatus {
|
||||
failed,
|
||||
|
||||
/// Message failed to updated
|
||||
// ignore: constant_identifier_names
|
||||
failed_update,
|
||||
|
||||
/// Message failed to delete
|
||||
// ignore: constant_identifier_names
|
||||
failed_delete,
|
||||
|
||||
/// Message correctly sent
|
||||
@@ -41,7 +42,46 @@ enum MessageSendingStatus {
|
||||
/// The class that contains the information about a message
|
||||
@JsonSerializable()
|
||||
class Message {
|
||||
/// The message ID. This is either created by Stream or set client side when the message is added.
|
||||
/// Constructor used for json serialization
|
||||
Message({
|
||||
String id,
|
||||
this.text,
|
||||
this.type,
|
||||
this.attachments,
|
||||
this.mentionedUsers,
|
||||
this.silent,
|
||||
this.shadowed,
|
||||
this.reactionCounts,
|
||||
this.reactionScores,
|
||||
this.latestReactions,
|
||||
this.ownReactions,
|
||||
this.parentId,
|
||||
this.quotedMessage,
|
||||
this.quotedMessageId,
|
||||
this.replyCount = 0,
|
||||
this.threadParticipants,
|
||||
this.showInChannel,
|
||||
this.command,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.user,
|
||||
this.pinned = false,
|
||||
this.pinnedAt,
|
||||
DateTime pinExpires,
|
||||
this.pinnedBy,
|
||||
this.extraData,
|
||||
this.deletedAt,
|
||||
this.status = MessageSendingStatus.sent,
|
||||
this.skipPush,
|
||||
}) : id = id ?? Uuid().v4(),
|
||||
pinExpires = pinExpires?.toUtc();
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// The message ID. This is either created by Stream or set client side when
|
||||
/// the message is added.
|
||||
final String id;
|
||||
|
||||
/// The text of this message
|
||||
@@ -55,7 +95,8 @@ class Message {
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
final String type;
|
||||
|
||||
/// The list of attachments, either provided by the user or generated from a command or as a result of URL scraping.
|
||||
/// The list of attachments, either provided by the user or generated from a
|
||||
/// command or as a result of URL scraping.
|
||||
@JsonKey(includeIfNull: false)
|
||||
final List<Attachment> attachments;
|
||||
|
||||
@@ -103,6 +144,9 @@ class Message {
|
||||
/// If true the message is silent
|
||||
final bool silent;
|
||||
|
||||
/// If true the message will not send a push notification
|
||||
final bool skipPush;
|
||||
|
||||
/// If true the message is shadowed
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
final bool shadowed;
|
||||
@@ -186,45 +230,9 @@ class Message {
|
||||
'pinned_at',
|
||||
'pin_expires',
|
||||
'pinned_by',
|
||||
'skip_push',
|
||||
];
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Message({
|
||||
String id,
|
||||
this.text,
|
||||
this.type,
|
||||
this.attachments,
|
||||
this.mentionedUsers,
|
||||
this.silent,
|
||||
this.shadowed,
|
||||
this.reactionCounts,
|
||||
this.reactionScores,
|
||||
this.latestReactions,
|
||||
this.ownReactions,
|
||||
this.parentId,
|
||||
this.quotedMessage,
|
||||
this.quotedMessageId,
|
||||
this.replyCount = 0,
|
||||
this.threadParticipants,
|
||||
this.showInChannel,
|
||||
this.command,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.user,
|
||||
this.pinned = false,
|
||||
this.pinnedAt,
|
||||
DateTime pinExpires,
|
||||
this.pinnedBy,
|
||||
this.extraData,
|
||||
this.deletedAt,
|
||||
this.status = MessageSendingStatus.sent,
|
||||
}) : id = id ?? Uuid().v4(),
|
||||
pinExpires = pinExpires?.toUtc();
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
_$MessageToJson(this), topLevelFields);
|
||||
@@ -259,6 +267,7 @@ class Message {
|
||||
User pinnedBy,
|
||||
Map<String, dynamic> extraData,
|
||||
MessageSendingStatus status,
|
||||
bool skipPush,
|
||||
}) {
|
||||
assert(() {
|
||||
if (pinExpires is! DateTime &&
|
||||
@@ -267,7 +276,7 @@ class Message {
|
||||
throw ArgumentError('`pinExpires` can only be set as DateTime or null');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}(), 'Validate type for pinExpires');
|
||||
return Message(
|
||||
id: id ?? this.id,
|
||||
text: text ?? this.text,
|
||||
@@ -297,11 +306,12 @@ class Message {
|
||||
pinnedAt: pinnedAt ?? this.pinnedAt,
|
||||
pinnedBy: pinnedBy ?? this.pinnedBy,
|
||||
pinExpires: pinExpires == _pinExpires ? this.pinExpires : pinExpires,
|
||||
skipPush: skipPush ?? this.skipPush,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a new [Message] that is a combination of this message and the given
|
||||
/// [other] message.
|
||||
/// Returns a new [Message] that is a combination of this message and the
|
||||
/// given [other] message.
|
||||
Message merge(Message other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
@@ -344,6 +354,12 @@ class TranslatedMessage extends Message {
|
||||
/// Constructor used for json serialization
|
||||
TranslatedMessage(this.i18n);
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory TranslatedMessage.fromJson(Map<String, dynamic> json) =>
|
||||
_$TranslatedMessageFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields),
|
||||
);
|
||||
|
||||
/// A Map of
|
||||
final Map<String, String> i18n;
|
||||
|
||||
@@ -354,13 +370,6 @@ class TranslatedMessage extends Message {
|
||||
...Message.topLevelFields,
|
||||
];
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory TranslatedMessage.fromJson(Map<String, dynamic> json) {
|
||||
return _$TranslatedMessageFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields),
|
||||
);
|
||||
}
|
||||
|
||||
/// Serialize to json
|
||||
@override
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
|
||||
@@ -93,6 +93,7 @@ Message _$MessageFromJson(Map json) {
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
skipPush: json['skip_push'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,6 +124,7 @@ Map<String, dynamic> _$MessageToJson(Message instance) {
|
||||
writeNotNull('thread_participants', readonly(instance.threadParticipants));
|
||||
val['show_in_channel'] = instance.showInChannel;
|
||||
val['silent'] = instance.silent;
|
||||
val['skip_push'] = instance.skipPush;
|
||||
writeNotNull('shadowed', readonly(instance.shadowed));
|
||||
writeNotNull('command', readonly(instance.command));
|
||||
writeNotNull('created_at', readonly(instance.createdAt));
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/channel_model.dart';
|
||||
|
||||
import 'serialization.dart';
|
||||
import 'user.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
|
||||
part 'mute.g.dart';
|
||||
|
||||
/// The class that contains the information about a muted user
|
||||
@JsonSerializable()
|
||||
class Mute {
|
||||
/// Constructor used for json serialization
|
||||
Mute({this.user, this.channel, this.createdAt, this.updatedAt});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Mute.fromJson(Map<String, dynamic> json) => _$MuteFromJson(json);
|
||||
|
||||
/// The user that performed the muting action
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
final User user;
|
||||
@@ -25,12 +30,6 @@ class Mute {
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Mute({this.user, this.channel, this.createdAt, this.updatedAt});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Mute.fromJson(Map<String, dynamic> json) => _$MuteFromJson(json);
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => _$MuteToJson(this);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'device.dart';
|
||||
import 'mute.dart';
|
||||
import 'serialization.dart';
|
||||
import 'user.dart';
|
||||
import 'package:stream_chat/src/models/device.dart';
|
||||
import 'package:stream_chat/src/models/mute.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
|
||||
part 'own_user.g.dart';
|
||||
|
||||
@@ -11,6 +10,36 @@ part 'own_user.g.dart';
|
||||
/// This object can be found in [Event]
|
||||
@JsonSerializable()
|
||||
class OwnUser extends User {
|
||||
/// Constructor used for json serialization
|
||||
OwnUser({
|
||||
this.devices,
|
||||
this.mutes,
|
||||
this.totalUnreadCount,
|
||||
this.unreadChannels,
|
||||
this.channelMutes,
|
||||
String id,
|
||||
String role,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime lastActive,
|
||||
bool online,
|
||||
Map<String, dynamic> extraData,
|
||||
bool banned,
|
||||
}) : super(
|
||||
id: id,
|
||||
role: role,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
lastActive: lastActive,
|
||||
online: online,
|
||||
extraData: extraData,
|
||||
banned: banned,
|
||||
);
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory OwnUser.fromJson(Map<String, dynamic> json) => _$OwnUserFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// List of user devices
|
||||
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
|
||||
final List<Device> devices;
|
||||
@@ -42,42 +71,8 @@ class OwnUser extends User {
|
||||
...User.topLevelFields,
|
||||
];
|
||||
|
||||
/// Constructor used for json serialization
|
||||
OwnUser({
|
||||
this.devices,
|
||||
this.mutes,
|
||||
this.totalUnreadCount,
|
||||
this.unreadChannels,
|
||||
this.channelMutes,
|
||||
String id,
|
||||
String role,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
DateTime lastActive,
|
||||
bool online,
|
||||
Map<String, dynamic> extraData,
|
||||
bool banned,
|
||||
}) : super(
|
||||
id: id,
|
||||
role: role,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
lastActive: lastActive,
|
||||
online: online,
|
||||
extraData: extraData,
|
||||
banned: banned,
|
||||
);
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory OwnUser.fromJson(Map<String, dynamic> json) {
|
||||
return _$OwnUserFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
}
|
||||
|
||||
/// Serialize to json
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return Serialization.moveFromExtraDataToRoot(
|
||||
_$OwnUserToJson(this), topLevelFields);
|
||||
}
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
_$OwnUserToJson(this), topLevelFields);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'serialization.dart';
|
||||
import 'user.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
|
||||
part 'reaction.g.dart';
|
||||
|
||||
/// The class that defines a reaction
|
||||
@JsonSerializable()
|
||||
class Reaction {
|
||||
/// Constructor used for json serialization
|
||||
Reaction({
|
||||
this.messageId,
|
||||
this.createdAt,
|
||||
this.type,
|
||||
this.user,
|
||||
String userId,
|
||||
this.score,
|
||||
this.extraData,
|
||||
}) : userId = userId ?? user?.id;
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Reaction.fromJson(Map<String, dynamic> json) => _$ReactionFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// The messageId to which the reaction belongs
|
||||
final String messageId;
|
||||
|
||||
@@ -43,28 +57,9 @@ class Reaction {
|
||||
'score',
|
||||
];
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Reaction({
|
||||
this.messageId,
|
||||
this.createdAt,
|
||||
this.type,
|
||||
this.user,
|
||||
String userId,
|
||||
this.score,
|
||||
this.extraData,
|
||||
}) : userId = userId ?? user?.id;
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Reaction.fromJson(Map<String, dynamic> json) {
|
||||
return _$ReactionFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
}
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() {
|
||||
return Serialization.moveFromExtraDataToRoot(
|
||||
_$ReactionToJson(this), topLevelFields);
|
||||
}
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
_$ReactionToJson(this), topLevelFields);
|
||||
|
||||
/// Creates a copy of [Reaction] with specified attributes overridden.
|
||||
Reaction copyWith({
|
||||
@@ -75,20 +70,19 @@ class Reaction {
|
||||
String userId,
|
||||
int score,
|
||||
Map<String, dynamic> extraData,
|
||||
}) {
|
||||
return Reaction(
|
||||
messageId: messageId ?? this.messageId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
type: type ?? this.type,
|
||||
user: user ?? this.user,
|
||||
userId: userId ?? this.userId,
|
||||
score: score ?? this.score,
|
||||
extraData: extraData ?? this.extraData,
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
Reaction(
|
||||
messageId: messageId ?? this.messageId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
type: type ?? this.type,
|
||||
user: user ?? this.user,
|
||||
userId: userId ?? this.userId,
|
||||
score: score ?? this.score,
|
||||
extraData: extraData ?? this.extraData,
|
||||
);
|
||||
|
||||
/// Returns a new [Reaction] that is a combination of this reaction and the given
|
||||
/// [other] reaction.
|
||||
/// Returns a new [Reaction] that is a combination of this reaction and the
|
||||
/// given [other] reaction.
|
||||
Reaction merge(Reaction other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'user.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
|
||||
part 'read.g.dart';
|
||||
|
||||
/// The class that defines a read event
|
||||
@JsonSerializable()
|
||||
class Read {
|
||||
/// Date of the read event
|
||||
final DateTime lastRead;
|
||||
|
||||
/// User who sent the event
|
||||
final User user;
|
||||
|
||||
/// Number of unread messages
|
||||
final int unreadMessages;
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Read({
|
||||
this.lastRead,
|
||||
@@ -26,6 +16,15 @@ class Read {
|
||||
/// Create a new instance from a json
|
||||
factory Read.fromJson(Map<String, dynamic> json) => _$ReadFromJson(json);
|
||||
|
||||
/// Date of the read event
|
||||
final DateTime lastRead;
|
||||
|
||||
/// User who sent the event
|
||||
final User user;
|
||||
|
||||
/// Number of unread messages
|
||||
final int unreadMessages;
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => _$ReadToJson(this);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'user.dart';
|
||||
import 'package:stream_chat/src/models/user.dart';
|
||||
|
||||
/// Used to avoid to serialize properties to json
|
||||
// ignore: prefer_void_to_null
|
||||
Null readonly(_) => null;
|
||||
|
||||
/// Helper class for serialization to and from json
|
||||
@@ -9,9 +10,8 @@ class Serialization {
|
||||
static const Function readOnly = readonly;
|
||||
|
||||
/// List of users to list of userIds
|
||||
static List<String> userIds(List<User> users) {
|
||||
return users?.map((u) => u.id)?.toList();
|
||||
}
|
||||
static List<String> userIds(List<User> users) =>
|
||||
users?.map((u) => u.id)?.toList();
|
||||
|
||||
/// Takes unknown json keys and puts them in the `extra_data` key
|
||||
static Map<String, dynamic> moveToExtraDataFromRoot(
|
||||
@@ -34,7 +34,8 @@ class Serialization {
|
||||
});
|
||||
}
|
||||
|
||||
/// Takes values in `extra_data` key and puts them on the root level of the json map
|
||||
/// Takes values in `extra_data` key and puts them on the root level of
|
||||
/// the json map
|
||||
static Map<String, dynamic> moveFromExtraDataToRoot(
|
||||
Map<String, dynamic> json,
|
||||
List<String> topLevelFields,
|
||||
|
||||
@@ -1,12 +1,53 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'serialization.dart';
|
||||
import 'package:stream_chat/src/models/serialization.dart';
|
||||
|
||||
part 'user.g.dart';
|
||||
|
||||
/// The class that defines the user model
|
||||
@JsonSerializable()
|
||||
class User {
|
||||
/// Constructor used for json serialization
|
||||
User({
|
||||
this.id,
|
||||
this.role,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.lastActive,
|
||||
this.online,
|
||||
this.extraData,
|
||||
this.banned,
|
||||
this.teams,
|
||||
});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
|
||||
/// Use this named constructor to create a new user instance
|
||||
User.init(
|
||||
this.id, {
|
||||
this.online,
|
||||
this.extraData,
|
||||
}) : createdAt = null,
|
||||
updatedAt = null,
|
||||
lastActive = null,
|
||||
banned = null,
|
||||
teams = null,
|
||||
role = null;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
static const topLevelFields = [
|
||||
'id',
|
||||
'role',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'last_active',
|
||||
'online',
|
||||
'banned',
|
||||
'teams',
|
||||
];
|
||||
|
||||
/// User id
|
||||
final String id;
|
||||
|
||||
@@ -42,43 +83,8 @@ class User {
|
||||
@JsonKey(includeIfNull: false)
|
||||
final Map<String, dynamic> extraData;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
static const topLevelFields = [
|
||||
'id',
|
||||
'role',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'last_active',
|
||||
'online',
|
||||
'banned',
|
||||
'teams',
|
||||
];
|
||||
|
||||
/// Use this named constructor to create a new user instance
|
||||
User.init(
|
||||
this.id, {
|
||||
this.online,
|
||||
this.extraData,
|
||||
}) : createdAt = null,
|
||||
updatedAt = null,
|
||||
lastActive = null,
|
||||
banned = null,
|
||||
teams = null,
|
||||
role = null;
|
||||
|
||||
/// Constructor used for json serialization
|
||||
User({
|
||||
this.id,
|
||||
this.role,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.lastActive,
|
||||
this.online,
|
||||
this.extraData,
|
||||
this.banned,
|
||||
this.teams,
|
||||
});
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
|
||||
/// Shortcut for user name
|
||||
String get name =>
|
||||
@@ -86,23 +92,12 @@ class User {
|
||||
? extraData['name']
|
||||
: id;
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory User.fromJson(Map<String, dynamic> json) {
|
||||
return _$UserFromJson(
|
||||
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
|
||||
}
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() {
|
||||
return Serialization.moveFromExtraDataToRoot(
|
||||
_$UserToJson(this), topLevelFields);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is User && runtimeType == other.runtimeType && id == other.id;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() =>
|
||||
Serialization.moveFromExtraDataToRoot(_$UserToJson(this), topLevelFields);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import 'platform_detector_stub.dart'
|
||||
import 'package:stream_chat/src/platform_detector/platform_detector_stub.dart'
|
||||
if (dart.library.html) 'platform_detector_web.dart'
|
||||
if (dart.library.io) 'platform_detector_io.dart';
|
||||
|
||||
/// Possible platforms
|
||||
enum PlatformType {
|
||||
///
|
||||
Android,
|
||||
android,
|
||||
|
||||
///
|
||||
Ios,
|
||||
ios,
|
||||
|
||||
///
|
||||
Web,
|
||||
web,
|
||||
|
||||
///
|
||||
MacOS,
|
||||
macOS,
|
||||
|
||||
///
|
||||
Windows,
|
||||
windows,
|
||||
|
||||
///
|
||||
Linux,
|
||||
linux,
|
||||
|
||||
///
|
||||
Fuchsia,
|
||||
fuchsia,
|
||||
}
|
||||
|
||||
/// Utility class that provides information on the current platform
|
||||
@@ -31,42 +31,42 @@ class CurrentPlatform {
|
||||
CurrentPlatform._();
|
||||
|
||||
/// True if the app is running on android
|
||||
static bool get isAndroid => type == PlatformType.Android;
|
||||
static bool get isAndroid => type == PlatformType.android;
|
||||
|
||||
/// True if the app is running on ios
|
||||
static bool get isIos => type == PlatformType.Ios;
|
||||
static bool get isIos => type == PlatformType.ios;
|
||||
|
||||
/// True if the app is running on web
|
||||
static bool get isWeb => type == PlatformType.Web;
|
||||
static bool get isWeb => type == PlatformType.web;
|
||||
|
||||
/// True if the app is running on macos
|
||||
static bool get isMacOS => type == PlatformType.MacOS;
|
||||
static bool get isMacOS => type == PlatformType.macOS;
|
||||
|
||||
/// True if the app is running on windows
|
||||
static bool get isWindows => type == PlatformType.Windows;
|
||||
static bool get isWindows => type == PlatformType.windows;
|
||||
|
||||
/// True if the app is running on linux
|
||||
static bool get isLinux => type == PlatformType.Linux;
|
||||
static bool get isLinux => type == PlatformType.linux;
|
||||
|
||||
/// True if the app is running on fuchsia
|
||||
static bool get isFuchsia => type == PlatformType.Fuchsia;
|
||||
static bool get isFuchsia => type == PlatformType.fuchsia;
|
||||
|
||||
/// Returns a string version of the platform
|
||||
static String get name {
|
||||
switch (type) {
|
||||
case PlatformType.Android:
|
||||
case PlatformType.android:
|
||||
return 'android';
|
||||
case PlatformType.Ios:
|
||||
case PlatformType.ios:
|
||||
return 'ios';
|
||||
case PlatformType.Web:
|
||||
case PlatformType.web:
|
||||
return 'web';
|
||||
case PlatformType.MacOS:
|
||||
case PlatformType.macOS:
|
||||
return 'macos';
|
||||
case PlatformType.Windows:
|
||||
case PlatformType.windows:
|
||||
return 'windows';
|
||||
case PlatformType.Linux:
|
||||
case PlatformType.linux:
|
||||
return 'linux';
|
||||
case PlatformType.Fuchsia:
|
||||
case PlatformType.fuchsia:
|
||||
return 'fuchsia';
|
||||
default:
|
||||
return '';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'dart:io';
|
||||
import 'platform_detector.dart';
|
||||
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
|
||||
|
||||
/// Version running on native systems
|
||||
PlatformType get currentPlatform {
|
||||
if (Platform.isWindows) return PlatformType.Windows;
|
||||
if (Platform.isFuchsia) return PlatformType.Fuchsia;
|
||||
if (Platform.isMacOS) return PlatformType.MacOS;
|
||||
if (Platform.isLinux) return PlatformType.Linux;
|
||||
if (Platform.isIOS) return PlatformType.Ios;
|
||||
return PlatformType.Android;
|
||||
if (Platform.isWindows) return PlatformType.windows;
|
||||
if (Platform.isFuchsia) return PlatformType.fuchsia;
|
||||
if (Platform.isMacOS) return PlatformType.macOS;
|
||||
if (Platform.isLinux) return PlatformType.linux;
|
||||
if (Platform.isIOS) return PlatformType.ios;
|
||||
return PlatformType.android;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'platform_detector.dart';
|
||||
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
|
||||
|
||||
/// Stub implementation
|
||||
PlatformType get currentPlatform {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'platform_detector.dart';
|
||||
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
|
||||
|
||||
/// Version running on web
|
||||
PlatformType get currentPlatform => PlatformType.Web;
|
||||
PlatformType get currentPlatform => PlatformType.web;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
library stream_chat;
|
||||
|
||||
export 'package:async/async.dart';
|
||||
export 'package:dio/src/dio_error.dart';
|
||||
export 'package:dio/src/multipart_file.dart';
|
||||
export 'package:dio/src/options.dart' show ProgressCallback;
|
||||
@@ -12,7 +13,10 @@ export './src/api/requests.dart';
|
||||
export './src/api/responses.dart';
|
||||
export './src/attachment_file_uploader.dart' show AttachmentFileUploader;
|
||||
export './src/client.dart';
|
||||
export './src/db/chat_persistence_client.dart';
|
||||
export './src/event_type.dart';
|
||||
export './src/extensions/rate_limit.dart';
|
||||
export './src/extensions/string_extension.dart';
|
||||
export './src/models/action.dart';
|
||||
export './src/models/attachment.dart';
|
||||
export './src/models/attachment_file.dart';
|
||||
@@ -29,5 +33,3 @@ export './src/models/own_user.dart';
|
||||
export './src/models/reaction.dart';
|
||||
export './src/models/read.dart';
|
||||
export './src/models/user.dart';
|
||||
export './src/extensions/string_extension.dart';
|
||||
export './src/db/chat_persistence_client.dart';
|
||||
|
||||
@@ -2,4 +2,5 @@ import 'package:stream_chat/src/client.dart';
|
||||
|
||||
/// Current package version
|
||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||
const PACKAGE_VERSION = '1.3.2+1-beta';
|
||||
// ignore: constant_identifier_names
|
||||
const PACKAGE_VERSION = '1.4.0-beta';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: stream_chat
|
||||
homepage: https://getstream.io/
|
||||
description: The official Dart client for Stream Chat, a service for building chat applications.
|
||||
version: 1.3.2+1-beta
|
||||
version: 1.4.0-beta
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
@@ -9,23 +9,22 @@ environment:
|
||||
sdk: ">=2.7.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
json_annotation: ^3.0.1
|
||||
logging: ^0.11.4
|
||||
dio: ^3.0.10
|
||||
web_socket_channel: ^1.1.0
|
||||
uuid: ^2.2.2
|
||||
async: ^2.4.2
|
||||
rxdart: ^0.25.0
|
||||
collection: ^1.14.13
|
||||
pedantic: ^1.9.2
|
||||
meta: ^1.2.4
|
||||
mime: ^0.9.7
|
||||
dio: ^3.0.10
|
||||
freezed_annotation: ^0.12.0
|
||||
http_parser: ^3.1.4
|
||||
json_annotation: ^3.0.1
|
||||
logging: ^0.11.4
|
||||
meta: ^1.2.4
|
||||
mime: ^0.9.7
|
||||
rxdart: ^0.25.0
|
||||
uuid: ^2.2.2
|
||||
web_socket_channel: ^1.1.0
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^1.10.0
|
||||
json_serializable: ^3.3.0
|
||||
test: ^1.15.7
|
||||
mockito: ^4.1.1
|
||||
freezed: ^0.12.7
|
||||
json_serializable: ^3.3.0
|
||||
mockito: ^4.1.1
|
||||
test: ^1.15.7
|
||||
|
||||
@@ -188,7 +188,7 @@ void main() {
|
||||
}
|
||||
};
|
||||
|
||||
final query = 'hello';
|
||||
const query = 'hello';
|
||||
|
||||
final queryParams = {
|
||||
'payload': json.encode({
|
||||
|
||||
@@ -897,6 +897,7 @@ void main() {
|
||||
"show_in_channel": null,
|
||||
"mentioned_users": [],
|
||||
"status": "SENT",
|
||||
"skip_push": null,
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
"pinned_at": null,
|
||||
@@ -913,6 +914,7 @@ void main() {
|
||||
"show_in_channel": null,
|
||||
"mentioned_users": [],
|
||||
"status": "SENT",
|
||||
"skip_push": null,
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
"pinned_at": null,
|
||||
@@ -922,6 +924,7 @@ void main() {
|
||||
{
|
||||
"id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0",
|
||||
"text": "test message",
|
||||
"skip_push": null,
|
||||
"attachments": [],
|
||||
"parent_id": null,
|
||||
"quoted_message": null,
|
||||
@@ -944,6 +947,7 @@ void main() {
|
||||
"quoted_message_id": null,
|
||||
"show_in_channel": null,
|
||||
"mentioned_users": [],
|
||||
"skip_push": null,
|
||||
"status": "SENT",
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
@@ -955,6 +959,7 @@ void main() {
|
||||
"id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe",
|
||||
"text": "test",
|
||||
"attachments": [],
|
||||
"skip_push": null,
|
||||
"parent_id": null,
|
||||
"quoted_message": null,
|
||||
"quoted_message_id": null,
|
||||
@@ -972,6 +977,7 @@ void main() {
|
||||
"text": "hi",
|
||||
"attachments": [],
|
||||
"parent_id": null,
|
||||
"skip_push": null,
|
||||
"quoted_message": null,
|
||||
"quoted_message_id": null,
|
||||
"show_in_channel": null,
|
||||
@@ -989,6 +995,7 @@ void main() {
|
||||
"attachments": [],
|
||||
"parent_id": null,
|
||||
"quoted_message": null,
|
||||
"skip_push": null,
|
||||
"quoted_message_id": null,
|
||||
"show_in_channel": null,
|
||||
"mentioned_users": [],
|
||||
@@ -1011,6 +1018,7 @@ void main() {
|
||||
"status": "SENT",
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
"skip_push": null,
|
||||
"pinned_at": null,
|
||||
"pin_expires": null,
|
||||
"pinned_by": null
|
||||
@@ -1028,6 +1036,7 @@ void main() {
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
"pinned_at": null,
|
||||
"skip_push": null,
|
||||
"pin_expires": null,
|
||||
"pinned_by": null
|
||||
},
|
||||
@@ -1039,6 +1048,7 @@ void main() {
|
||||
"quoted_message": null,
|
||||
"quoted_message_id": null,
|
||||
"show_in_channel": null,
|
||||
"skip_push": null,
|
||||
"mentioned_users": [],
|
||||
"status": "SENT",
|
||||
"silent": false,
|
||||
@@ -1056,6 +1066,7 @@ void main() {
|
||||
"quoted_message_id": null,
|
||||
"show_in_channel": null,
|
||||
"mentioned_users": [],
|
||||
"skip_push": null,
|
||||
"status": "SENT",
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
@@ -1074,6 +1085,7 @@ void main() {
|
||||
"mentioned_users": [],
|
||||
"status": "SENT",
|
||||
"silent": false,
|
||||
"skip_push": null,
|
||||
"pinned": false,
|
||||
"pinned_at": null,
|
||||
"pin_expires": null,
|
||||
@@ -1083,6 +1095,7 @@ void main() {
|
||||
"id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055",
|
||||
"text": "what are you doing?",
|
||||
"attachments": [],
|
||||
"skip_push": null,
|
||||
"parent_id": null,
|
||||
"quoted_message": null,
|
||||
"quoted_message_id": null,
|
||||
@@ -1100,6 +1113,7 @@ void main() {
|
||||
"text": "👍",
|
||||
"attachments": [],
|
||||
"parent_id": null,
|
||||
"skip_push": null,
|
||||
"quoted_message": null,
|
||||
"quoted_message_id": null,
|
||||
"show_in_channel": null,
|
||||
@@ -1115,6 +1129,7 @@ void main() {
|
||||
"id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21",
|
||||
"text": "sdasas",
|
||||
"attachments": [],
|
||||
"skip_push": null,
|
||||
"parent_id": null,
|
||||
"quoted_message": null,
|
||||
"quoted_message_id": null,
|
||||
@@ -1135,6 +1150,7 @@ void main() {
|
||||
"quoted_message": null,
|
||||
"quoted_message_id": null,
|
||||
"show_in_channel": null,
|
||||
"skip_push": null,
|
||||
"mentioned_users": [],
|
||||
"status": "SENT",
|
||||
"silent": false,
|
||||
@@ -1147,6 +1163,7 @@ void main() {
|
||||
"id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d",
|
||||
"text": "nhisagdhsadz",
|
||||
"attachments": [],
|
||||
"skip_push": null,
|
||||
"parent_id": null,
|
||||
"quoted_message": null,
|
||||
"quoted_message_id": null,
|
||||
@@ -1165,6 +1182,7 @@ void main() {
|
||||
"attachments": [],
|
||||
"parent_id": null,
|
||||
"quoted_message": null,
|
||||
"skip_push": null,
|
||||
"quoted_message_id": null,
|
||||
"show_in_channel": null,
|
||||
"mentioned_users": [],
|
||||
@@ -1181,6 +1199,7 @@ void main() {
|
||||
"attachments": [],
|
||||
"parent_id": null,
|
||||
"quoted_message": null,
|
||||
"skip_push": null,
|
||||
"quoted_message_id": null,
|
||||
"show_in_channel": null,
|
||||
"mentioned_users": [],
|
||||
@@ -1188,6 +1207,7 @@ void main() {
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
"pinned_at": null,
|
||||
"skip_push": null,
|
||||
"pin_expires": null,
|
||||
"pinned_by": null
|
||||
},
|
||||
@@ -1204,6 +1224,7 @@ void main() {
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
"pinned_at": null,
|
||||
"skip_push": null,
|
||||
"pin_expires": null,
|
||||
"pinned_by": null
|
||||
},
|
||||
@@ -1220,6 +1241,7 @@ void main() {
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
"pinned_at": null,
|
||||
"skip_push": null,
|
||||
"pin_expires": null,
|
||||
"pinned_by": null
|
||||
},
|
||||
@@ -1236,6 +1258,7 @@ void main() {
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
"pinned_at": null,
|
||||
"skip_push": null,
|
||||
"pin_expires": null,
|
||||
"pinned_by": null
|
||||
},
|
||||
@@ -1252,6 +1275,7 @@ void main() {
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
"pinned_at": null,
|
||||
"skip_push": null,
|
||||
"pin_expires": null,
|
||||
"pinned_by": null
|
||||
},
|
||||
@@ -1268,6 +1292,7 @@ void main() {
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
"pinned_at": null,
|
||||
"skip_push": null,
|
||||
"pin_expires": null,
|
||||
"pinned_by": null
|
||||
},
|
||||
@@ -1284,6 +1309,7 @@ void main() {
|
||||
"silent": false,
|
||||
"pinned": false,
|
||||
"pinned_at": null,
|
||||
"skip_push": null,
|
||||
"pin_expires": null,
|
||||
"pinned_by": null
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ void main() {
|
||||
"id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f",
|
||||
"text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA",
|
||||
"silent": false,
|
||||
"skip_push": null,
|
||||
"attachments": [
|
||||
{
|
||||
"type": "video",
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
## 1.4.0-beta
|
||||
|
||||
- Unfocus `MessageInput` only when sending commands
|
||||
- Updated default error for `MessageSearchListView`
|
||||
- Show error messages as system and keep them in the message input
|
||||
- Remove notification badge logic
|
||||
- Use shimmer while loading images
|
||||
- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated to `MessageInput`
|
||||
- Add possibility to specify custom message actions using `MessageWidget.customActions`
|
||||
- Added `MessageListView.onAttachmentTap` callback
|
||||
- Fixed message newline issue
|
||||
- Fixed `MessageListView` scroll keyboard behaviour
|
||||
- Minor fixes and improveqments
|
||||
|
||||
## 1.3.2-beta
|
||||
|
||||
- Updated `stream_chat_core` dependency
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/Users/salvatoregiordano/fvm/versions/beta
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"flutterSdkVersion": "beta"
|
||||
}
|
||||
@@ -34,6 +34,7 @@ android {
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:name=".Application"
|
||||
android:label="example"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.example.example
|
||||
|
||||
import io.flutter.app.FlutterApplication
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback
|
||||
import io.flutter.plugins.pathprovider.PathProviderPlugin
|
||||
|
||||
class Application : FlutterApplication(), PluginRegistrantCallback {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
override fun registerWith(registry: PluginRegistry?) {
|
||||
PathProviderPlugin.registerWith(registry?.registrarFor(
|
||||
"io.flutter.plugins.pathprovider.PathProviderPlugin"))
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -1,6 +1,12 @@
|
||||
package com.example.example
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugins.GeneratedPluginRegistrant
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
GeneratedPluginRegistrant.registerWith(flutterEngine);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
||||
classpath 'com.android.tools.build:gradle:3.6.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -44,7 +44,12 @@ class MyApp extends StatelessWidget {
|
||||
final defaultTheme = StreamChatThemeData.fromTheme(themeData);
|
||||
final colorTheme = defaultTheme.colorTheme;
|
||||
final customTheme = defaultTheme.merge(StreamChatThemeData(
|
||||
ownMessageTheme: MessageTheme(
|
||||
channelPreviewTheme: ChannelPreviewTheme(
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
otherMessageTheme: MessageTheme(
|
||||
messageBackgroundColor: colorTheme.black,
|
||||
messageText: TextStyle(
|
||||
color: colorTheme.white,
|
||||
|
||||
@@ -25,7 +25,11 @@ dependencies:
|
||||
sdk: flutter
|
||||
stream_chat_flutter:
|
||||
path: ../
|
||||
stream_chat_persistence: ^1.3.0-beta
|
||||
stream_chat_persistence:
|
||||
git:
|
||||
url: https://github.com/GetStream/stream-chat-flutter.git
|
||||
ref: develop
|
||||
path: packages/stream_chat_persistence
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 917 B |
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
Fore more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
-->
|
||||
<base href="/">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<script defer src="sql-wasm.js"></script>
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="example">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>example</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('flutter-first-frame', function () {
|
||||
navigator.serviceWorker.register('flutter_service_worker.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="main.dart.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "example",
|
||||
"short_name": "example",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
|
||||
// We are modularizing this manually because the current modularize setting in Emscripten has some issues:
|
||||
// https://github.com/kripken/emscripten/issues/5820
|
||||
// In addition, When you use emcc's modularization, it still expects to export a global object called `Module`,
|
||||
// which is able to be used/called before the WASM is loaded.
|
||||
// The modularization below exports a promise that loads and resolves to the actual sql.js module.
|
||||
// That way, this module can't be used before the WASM is finished loading.
|
||||
|
||||
// We are going to define a function that a user will call to start loading initializing our Sql.js library
|
||||
// However, that function might be called multiple times, and on subsequent calls, we don't actually want it to instantiate a new instance of the Module
|
||||
// Instead, we want to return the previously loaded module
|
||||
|
||||
// TODO: Make this not declare a global if used in the browser
|
||||
var initSqlJsPromise = undefined;
|
||||
|
||||
var initSqlJs = function (moduleConfig) {
|
||||
|
||||
if (initSqlJsPromise){
|
||||
return initSqlJsPromise;
|
||||
}
|
||||
// If we're here, we've never called this function before
|
||||
initSqlJsPromise = new Promise(function (resolveModule, reject) {
|
||||
|
||||
// We are modularizing this manually because the current modularize setting in Emscripten has some issues:
|
||||
// https://github.com/kripken/emscripten/issues/5820
|
||||
|
||||
// The way to affect the loading of emcc compiled modules is to create a variable called `Module` and add
|
||||
// properties to it, like `preRun`, `postRun`, etc
|
||||
// We are using that to get notified when the WASM has finished loading.
|
||||
// Only then will we return our promise
|
||||
|
||||
// If they passed in a moduleConfig object, use that
|
||||
// Otherwise, initialize Module to the empty object
|
||||
var Module = typeof moduleConfig !== 'undefined' ? moduleConfig : {};
|
||||
|
||||
// EMCC only allows for a single onAbort function (not an array of functions)
|
||||
// So if the user defined their own onAbort function, we remember it and call it
|
||||
var originalOnAbortFunction = Module['onAbort'];
|
||||
Module['onAbort'] = function (errorThatCausedAbort) {
|
||||
reject(new Error(errorThatCausedAbort));
|
||||
if (originalOnAbortFunction){
|
||||
originalOnAbortFunction(errorThatCausedAbort);
|
||||
}
|
||||
};
|
||||
|
||||
Module['postRun'] = Module['postRun'] || [];
|
||||
Module['postRun'].push(function () {
|
||||
// When Emscripted calls postRun, this promise resolves with the built Module
|
||||
resolveModule(Module);
|
||||
});
|
||||
|
||||
// There is a section of code in the emcc-generated code below that looks like this:
|
||||
// (Note that this is lowercase `module`)
|
||||
// if (typeof module !== 'undefined') {
|
||||
// module['exports'] = Module;
|
||||
// }
|
||||
// When that runs, it's going to overwrite our own modularization export efforts in shell-post.js!
|
||||
// The only way to tell emcc not to emit it is to pass the MODULARIZE=1 or MODULARIZE_INSTANCE=1 flags,
|
||||
// but that carries with it additional unnecessary baggage/bugs we don't want either.
|
||||
// So, we have three options:
|
||||
// 1) We undefine `module`
|
||||
// 2) We remember what `module['exports']` was at the beginning of this function and we restore it later
|
||||
// 3) We write a script to remove those lines of code as part of the Make process.
|
||||
//
|
||||
// Since those are the only lines of code that care about module, we will undefine it. It's the most straightforward
|
||||
// of the options, and has the side effect of reducing emcc's efforts to modify the module if its output were to change in the future.
|
||||
// That's a nice side effect since we're handling the modularization efforts ourselves
|
||||
module = undefined;
|
||||
|
||||
// The emcc-generated code and shell-post.js code goes below,
|
||||
// meaning that all of it runs inside of this promise. If anything throws an exception, our promise will abort
|
||||
|
||||
var e;e||(e=typeof Module !== 'undefined' ? Module : {});null;
|
||||
e.onRuntimeInitialized=function(){function a(h,l){this.Ra=h;this.db=l;this.Qa=1;this.lb=[]}function b(h,l){this.db=l;l=aa(h)+1;this.eb=ba(l);if(null===this.eb)throw Error("Unable to allocate memory for the SQL string");k(h,m,this.eb,l);this.jb=this.eb;this.$a=this.pb=null}function c(h){this.filename="dbfile_"+(4294967295*Math.random()>>>0);if(null!=h){var l=this.filename,p=l?r("//"+l):"/";l=ca(!0,!0);p=da(p,(void 0!==l?l:438)&4095|32768,0);if(h){if("string"===typeof h){for(var q=Array(h.length),B=
|
||||
0,ha=h.length;B<ha;++B)q[B]=h.charCodeAt(B);h=q}ea(p,l|146);q=u(p,"w");fa(q,h,0,h.length,0,void 0);ka(q);ea(p,l)}}this.handleError(g(this.filename,d));this.db=x(d,"i32");nc(this.db);this.fb={};this.Xa={}}var d=y(4),f=e.cwrap,g=f("sqlite3_open","number",["string","number"]),n=f("sqlite3_close_v2","number",["number"]),t=f("sqlite3_exec","number",["number","string","number","number","number"]),w=f("sqlite3_changes","number",["number"]),v=f("sqlite3_prepare_v2","number",["number","string","number","number",
|
||||
"number"]),C=f("sqlite3_sql","string",["number"]),H=f("sqlite3_normalized_sql","string",["number"]),Y=f("sqlite3_prepare_v2","number",["number","number","number","number","number"]),oc=f("sqlite3_bind_text","number",["number","number","number","number","number"]),pb=f("sqlite3_bind_blob","number",["number","number","number","number","number"]),pc=f("sqlite3_bind_double","number",["number","number","number"]),qc=f("sqlite3_bind_int","number",["number","number","number"]),rc=f("sqlite3_bind_parameter_index",
|
||||
"number",["number","string"]),sc=f("sqlite3_step","number",["number"]),tc=f("sqlite3_errmsg","string",["number"]),uc=f("sqlite3_column_count","number",["number"]),vc=f("sqlite3_data_count","number",["number"]),wc=f("sqlite3_column_double","number",["number","number"]),xc=f("sqlite3_column_text","string",["number","number"]),yc=f("sqlite3_column_blob","number",["number","number"]),zc=f("sqlite3_column_bytes","number",["number","number"]),Ac=f("sqlite3_column_type","number",["number","number"]),Bc=
|
||||
f("sqlite3_column_name","string",["number","number"]),Cc=f("sqlite3_reset","number",["number"]),Dc=f("sqlite3_clear_bindings","number",["number"]),Ec=f("sqlite3_finalize","number",["number"]),Fc=f("sqlite3_create_function_v2","number","number string number number number number number number number".split(" ")),Gc=f("sqlite3_value_type","number",["number"]),Hc=f("sqlite3_value_bytes","number",["number"]),Ic=f("sqlite3_value_text","string",["number"]),Jc=f("sqlite3_value_blob","number",["number"]),
|
||||
Kc=f("sqlite3_value_double","number",["number"]),Lc=f("sqlite3_result_double","",["number","number"]),qb=f("sqlite3_result_null","",["number"]),Mc=f("sqlite3_result_text","",["number","string","number","number"]),Nc=f("sqlite3_result_blob","",["number","number","number","number"]),Oc=f("sqlite3_result_int","",["number","number"]),rb=f("sqlite3_result_error","",["number","string","number"]),nc=f("RegisterExtensionFunctions","number",["number"]);a.prototype.bind=function(h){if(!this.Ra)throw"Statement closed";
|
||||
this.reset();return Array.isArray(h)?this.Bb(h):null!=h&&"object"===typeof h?this.Cb(h):!0};a.prototype.step=function(){if(!this.Ra)throw"Statement closed";this.Qa=1;var h=sc(this.Ra);switch(h){case 100:return!0;case 101:return!1;default:throw this.db.handleError(h);}};a.prototype.Ib=function(h){null==h&&(h=this.Qa,this.Qa+=1);return wc(this.Ra,h)};a.prototype.Jb=function(h){null==h&&(h=this.Qa,this.Qa+=1);return xc(this.Ra,h)};a.prototype.getBlob=function(h){null==h&&(h=this.Qa,this.Qa+=1);var l=
|
||||
zc(this.Ra,h);h=yc(this.Ra,h);for(var p=new Uint8Array(l),q=0;q<l;q+=1)p[q]=z[h+q];return p};a.prototype.get=function(h){null!=h&&this.bind(h)&&this.step();h=[];for(var l=vc(this.Ra),p=0;p<l;p+=1)switch(Ac(this.Ra,p)){case 1:case 2:h.push(this.Ib(p));break;case 3:h.push(this.Jb(p));break;case 4:h.push(this.getBlob(p));break;default:h.push(null)}return h};a.prototype.getColumnNames=function(){for(var h=[],l=uc(this.Ra),p=0;p<l;p+=1)h.push(Bc(this.Ra,p));return h};a.prototype.getAsObject=function(h){h=
|
||||
this.get(h);for(var l=this.getColumnNames(),p={},q=0;q<l.length;q+=1)p[l[q]]=h[q];return p};a.prototype.getSQL=function(){return C(this.Ra)};a.prototype.getNormalizedSQL=function(){return H(this.Ra)};a.prototype.run=function(h){null!=h&&this.bind(h);this.step();return this.reset()};a.prototype.Fb=function(h,l){null==l&&(l=this.Qa,this.Qa+=1);h=la(h);var p=ma(h);this.lb.push(p);this.db.handleError(oc(this.Ra,l,p,h.length-1,0))};a.prototype.Ab=function(h,l){null==l&&(l=this.Qa,this.Qa+=1);var p=ma(h);
|
||||
this.lb.push(p);this.db.handleError(pb(this.Ra,l,p,h.length,0))};a.prototype.Eb=function(h,l){null==l&&(l=this.Qa,this.Qa+=1);this.db.handleError((h===(h|0)?qc:pc)(this.Ra,l,h))};a.prototype.Db=function(h){null==h&&(h=this.Qa,this.Qa+=1);pb(this.Ra,h,0,0,0)};a.prototype.tb=function(h,l){null==l&&(l=this.Qa,this.Qa+=1);switch(typeof h){case "string":this.Fb(h,l);return;case "number":case "boolean":this.Eb(h+0,l);return;case "object":if(null===h){this.Db(l);return}if(null!=h.length){this.Ab(h,l);return}}throw"Wrong API use : tried to bind a value of an unknown type ("+
|
||||
h+").";};a.prototype.Cb=function(h){var l=this;Object.keys(h).forEach(function(p){var q=rc(l.Ra,p);0!==q&&l.tb(h[p],q)});return!0};a.prototype.Bb=function(h){for(var l=0;l<h.length;l+=1)this.tb(h[l],l+1);return!0};a.prototype.reset=function(){return 0===Dc(this.Ra)&&0===Cc(this.Ra)};a.prototype.freemem=function(){for(var h;void 0!==(h=this.lb.pop());)na(h)};a.prototype.free=function(){var h=0===Ec(this.Ra);delete this.db.fb[this.Ra];this.Ra=0;return h};b.prototype.next=function(){if(null===this.eb)return{done:!0};
|
||||
null!==this.$a&&(this.$a.free(),this.$a=null);if(!this.db.db)throw this.nb(),Error("Database closed");var h=oa(),l=y(4);pa(d);pa(l);try{this.db.handleError(Y(this.db.db,this.jb,-1,d,l));this.jb=x(l,"i32");var p=x(d,"i32");if(0===p)return this.nb(),{done:!0};this.$a=new a(p,this.db);this.db.fb[p]=this.$a;return{value:this.$a,done:!1}}catch(q){throw this.pb=A(this.jb),this.nb(),q;}finally{qa(h)}};b.prototype.nb=function(){na(this.eb);this.eb=null};b.prototype.getRemainingSQL=function(){return null!==
|
||||
this.pb?this.pb:A(this.jb)};"function"===typeof Symbol&&"symbol"===typeof Symbol.iterator&&(b.prototype[Symbol.iterator]=function(){return this});c.prototype.run=function(h,l){if(!this.db)throw"Database closed";if(l){h=this.prepare(h,l);try{h.step()}finally{h.free()}}else this.handleError(t(this.db,h,0,0,d));return this};c.prototype.exec=function(h,l){if(!this.db)throw"Database closed";var p=oa(),q=null;try{var B=aa(h)+1,ha=y(B);k(h,z,ha,B);var D=ha;var ia=y(4);for(h=[];0!==x(D,"i8");){pa(d);pa(ia);
|
||||
this.handleError(Y(this.db,D,-1,d,ia));var ja=x(d,"i32");D=x(ia,"i32");if(0!==ja){B=null;q=new a(ja,this);for(null!=l&&q.bind(l);q.step();)null===B&&(B={columns:q.getColumnNames(),values:[]},h.push(B)),B.values.push(q.get());q.free()}}return h}catch(E){throw q&&q.free(),E;}finally{qa(p)}};c.prototype.each=function(h,l,p,q){"function"===typeof l&&(q=p,p=l,l=void 0);h=this.prepare(h,l);try{for(;h.step();)p(h.getAsObject())}finally{h.free()}if("function"===typeof q)return q()};c.prototype.prepare=function(h,
|
||||
l){pa(d);this.handleError(v(this.db,h,-1,d,0));h=x(d,"i32");if(0===h)throw"Nothing to prepare";var p=new a(h,this);null!=l&&p.bind(l);return this.fb[h]=p};c.prototype.iterateStatements=function(h){return new b(h,this)};c.prototype["export"]=function(){Object.values(this.fb).forEach(function(l){l.free()});Object.values(this.Xa).forEach(ra);this.Xa={};this.handleError(n(this.db));var h=sa(this.filename);this.handleError(g(this.filename,d));this.db=x(d,"i32");return h};c.prototype.close=function(){null!==
|
||||
this.db&&(Object.values(this.fb).forEach(function(h){h.free()}),Object.values(this.Xa).forEach(ra),this.Xa={},this.handleError(n(this.db)),ta("/"+this.filename),this.db=null)};c.prototype.handleError=function(h){if(0===h)return null;h=tc(this.db);throw Error(h);};c.prototype.getRowsModified=function(){return w(this.db)};c.prototype.create_function=function(h,l){Object.prototype.hasOwnProperty.call(this.Xa,h)&&(ua(this.Xa[h]),delete this.Xa[h]);var p=va(function(q,B,ha){for(var D,ia=[],ja=0;ja<B;ja+=
|
||||
1){var E=x(ha+4*ja,"i32"),R=Gc(E);if(1===R||2===R)E=Kc(E);else if(3===R)E=Ic(E);else if(4===R){R=E;E=Hc(R);R=Jc(R);for(var wb=new Uint8Array(E),Ea=0;Ea<E;Ea+=1)wb[Ea]=z[R+Ea];E=wb}else E=null;ia.push(E)}try{D=l.apply(null,ia)}catch(Rc){rb(q,Rc,-1);return}switch(typeof D){case "boolean":Oc(q,D?1:0);break;case "number":Lc(q,D);break;case "string":Mc(q,D,-1,-1);break;case "object":null===D?qb(q):null!=D.length?(B=ma(D),Nc(q,B,D.length,-1),na(B)):rb(q,"Wrong API use : tried to return a value of an unknown type ("+
|
||||
D+").",-1);break;default:qb(q)}});this.Xa[h]=p;this.handleError(Fc(this.db,h,l.length,1,0,p,0,0,0));return this};e.Database=c};var wa={},F;for(F in e)e.hasOwnProperty(F)&&(wa[F]=e[F]);var xa="./this.program",ya=!1,G=!1,za=!1,Aa=!1;ya="object"===typeof window;G="function"===typeof importScripts;za="object"===typeof process&&"object"===typeof process.versions&&"string"===typeof process.versions.node;Aa=!ya&&!za&&!G;var I="",Ba,Ca,Da,Fa;
|
||||
if(za)I=G?require("path").dirname(I)+"/":__dirname+"/",Ba=function(a,b){Da||(Da=require("fs"));Fa||(Fa=require("path"));a=Fa.normalize(a);return Da.readFileSync(a,b?null:"utf8")},Ca=function(a){a=Ba(a,!0);a.buffer||(a=new Uint8Array(a));assert(a.buffer);return a},1<process.argv.length&&(xa=process.argv[1].replace(/\\/g,"/")),process.argv.slice(2),"undefined"!==typeof module&&(module.exports=e),e.inspect=function(){return"[Emscripten Module object]"};else if(Aa)"undefined"!=typeof read&&(Ba=function(a){return read(a)}),
|
||||
Ca=function(a){if("function"===typeof readbuffer)return new Uint8Array(readbuffer(a));a=read(a,"binary");assert("object"===typeof a);return a},"undefined"!==typeof print&&("undefined"===typeof console&&(console={}),console.log=print,console.warn=console.error="undefined"!==typeof printErr?printErr:print);else if(ya||G)G?I=self.location.href:document.currentScript&&(I=document.currentScript.src),I=0!==I.indexOf("blob:")?I.substr(0,I.lastIndexOf("/")+1):"",Ba=function(a){var b=new XMLHttpRequest;b.open("GET",
|
||||
a,!1);b.send(null);return b.responseText},G&&(Ca=function(a){var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)});var Ga=e.print||console.log.bind(console),J=e.printErr||console.warn.bind(console);for(F in wa)wa.hasOwnProperty(F)&&(e[F]=wa[F]);wa=null;e.thisProgram&&(xa=e.thisProgram);var Ha=[],Ia;function ua(a){Ia.delete(Ja.get(a));Ha.push(a)}
|
||||
function va(a){var b=Ja;if(!Ia){Ia=new WeakMap;for(var c=0;c<b.length;c++){var d=b.get(c);d&&Ia.set(d,c)}}if(Ia.has(a))a=Ia.get(a);else{if(Ha.length)c=Ha.pop();else{c=b.length;try{b.grow(1)}catch(n){if(!(n instanceof RangeError))throw n;throw"Unable to grow wasm table. Set ALLOW_TABLE_GROWTH.";}}try{b.set(c,a)}catch(n){if(!(n instanceof TypeError))throw n;if("function"===typeof WebAssembly.Function){var f={i:"i32",j:"i64",f:"f32",d:"f64"},g={parameters:[],results:[]};for(d=1;4>d;++d)g.parameters.push(f["viii"[d]]);
|
||||
d=new WebAssembly.Function(g,a)}else{f=[1,0,1,96];g={i:127,j:126,f:125,d:124};f.push(3);for(d=0;3>d;++d)f.push(g["iii"[d]]);f.push(0);f[1]=f.length-2;d=new Uint8Array([0,97,115,109,1,0,0,0].concat(f,[2,7,1,1,101,1,102,0,0,7,5,1,1,102,0,0]));d=new WebAssembly.Module(d);d=(new WebAssembly.Instance(d,{e:{f:a}})).exports.f}b.set(c,d)}Ia.set(a,c);a=c}return a}function ra(a){ua(a)}var Ka;e.wasmBinary&&(Ka=e.wasmBinary);var noExitRuntime;e.noExitRuntime&&(noExitRuntime=e.noExitRuntime);
|
||||
"object"!==typeof WebAssembly&&K("no native wasm support detected");
|
||||
function pa(a){var b="i32";"*"===b.charAt(b.length-1)&&(b="i32");switch(b){case "i1":z[a>>0]=0;break;case "i8":z[a>>0]=0;break;case "i16":La[a>>1]=0;break;case "i32":L[a>>2]=0;break;case "i64":M=[0,(N=0,1<=+Math.abs(N)?0<N?(Math.min(+Math.floor(N/4294967296),4294967295)|0)>>>0:~~+Math.ceil((N-+(~~N>>>0))/4294967296)>>>0:0)];L[a>>2]=M[0];L[a+4>>2]=M[1];break;case "float":Ma[a>>2]=0;break;case "double":Na[a>>3]=0;break;default:K("invalid type for setValue: "+b)}}
|
||||
function x(a,b){b=b||"i8";"*"===b.charAt(b.length-1)&&(b="i32");switch(b){case "i1":return z[a>>0];case "i8":return z[a>>0];case "i16":return La[a>>1];case "i32":return L[a>>2];case "i64":return L[a>>2];case "float":return Ma[a>>2];case "double":return Na[a>>3];default:K("invalid type for getValue: "+b)}return null}var Oa,Ja,Pa=!1;function assert(a,b){a||K("Assertion failed: "+b)}function Qa(a){var b=e["_"+a];assert(b,"Cannot call unknown function "+a+", make sure it is exported");return b}
|
||||
function Ra(a,b,c,d){var f={string:function(v){var C=0;if(null!==v&&void 0!==v&&0!==v){var H=(v.length<<2)+1;C=y(H);k(v,m,C,H)}return C},array:function(v){var C=y(v.length);z.set(v,C);return C}},g=Qa(a),n=[];a=0;if(d)for(var t=0;t<d.length;t++){var w=f[c[t]];w?(0===a&&(a=oa()),n[t]=w(d[t])):n[t]=d[t]}c=g.apply(null,n);c=function(v){return"string"===b?A(v):"boolean"===b?!!v:v}(c);0!==a&&qa(a);return c}var Sa=0,Ta=1;
|
||||
function ma(a){var b=Sa==Ta?y(a.length):ba(a.length);a.subarray||a.slice?m.set(a,b):m.set(new Uint8Array(a),b);return b}var Ua="undefined"!==typeof TextDecoder?new TextDecoder("utf8"):void 0;
|
||||
function Va(a,b,c){var d=b+c;for(c=b;a[c]&&!(c>=d);)++c;if(16<c-b&&a.subarray&&Ua)return Ua.decode(a.subarray(b,c));for(d="";b<c;){var f=a[b++];if(f&128){var g=a[b++]&63;if(192==(f&224))d+=String.fromCharCode((f&31)<<6|g);else{var n=a[b++]&63;f=224==(f&240)?(f&15)<<12|g<<6|n:(f&7)<<18|g<<12|n<<6|a[b++]&63;65536>f?d+=String.fromCharCode(f):(f-=65536,d+=String.fromCharCode(55296|f>>10,56320|f&1023))}}else d+=String.fromCharCode(f)}return d}function A(a,b){return a?Va(m,a,b):""}
|
||||
function k(a,b,c,d){if(!(0<d))return 0;var f=c;d=c+d-1;for(var g=0;g<a.length;++g){var n=a.charCodeAt(g);if(55296<=n&&57343>=n){var t=a.charCodeAt(++g);n=65536+((n&1023)<<10)|t&1023}if(127>=n){if(c>=d)break;b[c++]=n}else{if(2047>=n){if(c+1>=d)break;b[c++]=192|n>>6}else{if(65535>=n){if(c+2>=d)break;b[c++]=224|n>>12}else{if(c+3>=d)break;b[c++]=240|n>>18;b[c++]=128|n>>12&63}b[c++]=128|n>>6&63}b[c++]=128|n&63}}b[c]=0;return c-f}
|
||||
function aa(a){for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);55296<=d&&57343>=d&&(d=65536+((d&1023)<<10)|a.charCodeAt(++c)&1023);127>=d?++b:b=2047>=d?b+2:65535>=d?b+3:b+4}return b}function Wa(a){var b=aa(a)+1,c=ba(b);c&&k(a,z,c,b);return c}var Xa,z,m,La,L,Ma,Na;
|
||||
function Ya(a){Xa=a;e.HEAP8=z=new Int8Array(a);e.HEAP16=La=new Int16Array(a);e.HEAP32=L=new Int32Array(a);e.HEAPU8=m=new Uint8Array(a);e.HEAPU16=new Uint16Array(a);e.HEAPU32=new Uint32Array(a);e.HEAPF32=Ma=new Float32Array(a);e.HEAPF64=Na=new Float64Array(a)}var Za=e.INITIAL_MEMORY||16777216;e.wasmMemory?Oa=e.wasmMemory:Oa=new WebAssembly.Memory({initial:Za/65536,maximum:32768});Oa&&(Xa=Oa.buffer);Za=Xa.byteLength;Ya(Xa);var $a=[],ab=[],bb=[],cb=[];
|
||||
function db(){var a=e.preRun.shift();$a.unshift(a)}var eb=0,fb=null,gb=null;e.preloadedImages={};e.preloadedAudios={};function K(a){if(e.onAbort)e.onAbort(a);J(a);Pa=!0;throw new WebAssembly.RuntimeError("abort("+a+"). Build with -s ASSERTIONS=1 for more info.");}function hb(a){var b=ib;return String.prototype.startsWith?b.startsWith(a):0===b.indexOf(a)}function jb(){return hb("data:application/octet-stream;base64,")}var ib="sql-wasm.wasm";
|
||||
if(!jb()){var kb=ib;ib=e.locateFile?e.locateFile(kb,I):I+kb}function lb(){try{if(Ka)return new Uint8Array(Ka);if(Ca)return Ca(ib);throw"both async and sync fetching of the wasm failed";}catch(a){K(a)}}function mb(){return Ka||!ya&&!G||"function"!==typeof fetch||hb("file://")?Promise.resolve().then(lb):fetch(ib,{credentials:"same-origin"}).then(function(a){if(!a.ok)throw"failed to load wasm binary file at '"+ib+"'";return a.arrayBuffer()}).catch(function(){return lb()})}var N,M;
|
||||
function nb(a){for(;0<a.length;){var b=a.shift();if("function"==typeof b)b(e);else{var c=b.Hb;"number"===typeof c?void 0===b.mb?Ja.get(c)():Ja.get(c)(b.mb):c(void 0===b.mb?null:b.mb)}}}function ob(a){return a.replace(/\b_Z[\w\d_]+/g,function(b){return b===b?b:b+" ["+b+"]"})}
|
||||
function sb(){function a(g){return(g=g.toTimeString().match(/\(([A-Za-z ]+)\)$/))?g[1]:"GMT"}if(!tb){tb=!0;L[ub()>>2]=60*(new Date).getTimezoneOffset();var b=(new Date).getFullYear(),c=new Date(b,0,1);b=new Date(b,6,1);L[vb()>>2]=Number(c.getTimezoneOffset()!=b.getTimezoneOffset());var d=a(c),f=a(b);d=Wa(d);f=Wa(f);b.getTimezoneOffset()<c.getTimezoneOffset()?(L[xb()>>2]=d,L[xb()+4>>2]=f):(L[xb()>>2]=f,L[xb()+4>>2]=d)}}var tb;
|
||||
function yb(a,b){for(var c=0,d=a.length-1;0<=d;d--){var f=a[d];"."===f?a.splice(d,1):".."===f?(a.splice(d,1),c++):c&&(a.splice(d,1),c--)}if(b)for(;c;c--)a.unshift("..");return a}function r(a){var b="/"===a.charAt(0),c="/"===a.substr(-1);(a=yb(a.split("/").filter(function(d){return!!d}),!b).join("/"))||b||(a=".");a&&c&&(a+="/");return(b?"/":"")+a}
|
||||
function zb(a){var b=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(a).slice(1);a=b[0];b=b[1];if(!a&&!b)return".";b&&(b=b.substr(0,b.length-1));return a+b}function Ab(a){if("/"===a)return"/";a=r(a);a=a.replace(/\/$/,"");var b=a.lastIndexOf("/");return-1===b?a:a.substr(b+1)}function Bb(a){L[Cb()>>2]=a}
|
||||
function Db(){if("object"===typeof crypto&&"function"===typeof crypto.getRandomValues){var a=new Uint8Array(1);return function(){crypto.getRandomValues(a);return a[0]}}if(za)try{var b=require("crypto");return function(){return b.randomBytes(1)[0]}}catch(c){}return function(){K("randomDevice")}}
|
||||
function Eb(){for(var a="",b=!1,c=arguments.length-1;-1<=c&&!b;c--){b=0<=c?arguments[c]:"/";if("string"!==typeof b)throw new TypeError("Arguments to path.resolve must be strings");if(!b)return"";a=b+"/"+a;b="/"===b.charAt(0)}a=yb(a.split("/").filter(function(d){return!!d}),!b).join("/");return(b?"/":"")+a||"."}var Fb=[];function Gb(a,b){Fb[a]={input:[],output:[],cb:b};Hb(a,Ib)}
|
||||
var Ib={open:function(a){var b=Fb[a.node.rdev];if(!b)throw new O(43);a.tty=b;a.seekable=!1},close:function(a){a.tty.cb.flush(a.tty)},flush:function(a){a.tty.cb.flush(a.tty)},read:function(a,b,c,d){if(!a.tty||!a.tty.cb.xb)throw new O(60);for(var f=0,g=0;g<d;g++){try{var n=a.tty.cb.xb(a.tty)}catch(t){throw new O(29);}if(void 0===n&&0===f)throw new O(6);if(null===n||void 0===n)break;f++;b[c+g]=n}f&&(a.node.timestamp=Date.now());return f},write:function(a,b,c,d){if(!a.tty||!a.tty.cb.qb)throw new O(60);
|
||||
try{for(var f=0;f<d;f++)a.tty.cb.qb(a.tty,b[c+f])}catch(g){throw new O(29);}d&&(a.node.timestamp=Date.now());return f}},Jb={xb:function(a){if(!a.input.length){var b=null;if(za){var c=Buffer.zb?Buffer.zb(256):new Buffer(256),d=0;try{d=Da.readSync(process.stdin.fd,c,0,256,null)}catch(f){if(-1!=f.toString().indexOf("EOF"))d=0;else throw f;}0<d?b=c.slice(0,d).toString("utf-8"):b=null}else"undefined"!=typeof window&&"function"==typeof window.prompt?(b=window.prompt("Input: "),null!==b&&(b+="\n")):"function"==
|
||||
typeof readline&&(b=readline(),null!==b&&(b+="\n"));if(!b)return null;a.input=la(b,!0)}return a.input.shift()},qb:function(a,b){null===b||10===b?(Ga(Va(a.output,0)),a.output=[]):0!=b&&a.output.push(b)},flush:function(a){a.output&&0<a.output.length&&(Ga(Va(a.output,0)),a.output=[])}},Kb={qb:function(a,b){null===b||10===b?(J(Va(a.output,0)),a.output=[]):0!=b&&a.output.push(b)},flush:function(a){a.output&&0<a.output.length&&(J(Va(a.output,0)),a.output=[])}},P={Va:null,Wa:function(){return P.createNode(null,
|
||||
"/",16895,0)},createNode:function(a,b,c,d){if(24576===(c&61440)||4096===(c&61440))throw new O(63);P.Va||(P.Va={dir:{node:{Ua:P.Na.Ua,Ta:P.Na.Ta,lookup:P.Na.lookup,gb:P.Na.gb,rename:P.Na.rename,unlink:P.Na.unlink,rmdir:P.Na.rmdir,readdir:P.Na.readdir,symlink:P.Na.symlink},stream:{Za:P.Oa.Za}},file:{node:{Ua:P.Na.Ua,Ta:P.Na.Ta},stream:{Za:P.Oa.Za,read:P.Oa.read,write:P.Oa.write,sb:P.Oa.sb,hb:P.Oa.hb,ib:P.Oa.ib}},link:{node:{Ua:P.Na.Ua,Ta:P.Na.Ta,readlink:P.Na.readlink},stream:{}},ub:{node:{Ua:P.Na.Ua,
|
||||
Ta:P.Na.Ta},stream:Lb}});c=Mb(a,b,c,d);Q(c.mode)?(c.Na=P.Va.dir.node,c.Oa=P.Va.dir.stream,c.Ma={}):32768===(c.mode&61440)?(c.Na=P.Va.file.node,c.Oa=P.Va.file.stream,c.Sa=0,c.Ma=null):40960===(c.mode&61440)?(c.Na=P.Va.link.node,c.Oa=P.Va.link.stream):8192===(c.mode&61440)&&(c.Na=P.Va.ub.node,c.Oa=P.Va.ub.stream);c.timestamp=Date.now();a&&(a.Ma[b]=c);return c},Sb:function(a){if(a.Ma&&a.Ma.subarray){for(var b=[],c=0;c<a.Sa;++c)b.push(a.Ma[c]);return b}return a.Ma},Tb:function(a){return a.Ma?a.Ma.subarray?
|
||||
a.Ma.subarray(0,a.Sa):new Uint8Array(a.Ma):new Uint8Array(0)},vb:function(a,b){var c=a.Ma?a.Ma.length:0;c>=b||(b=Math.max(b,c*(1048576>c?2:1.125)>>>0),0!=c&&(b=Math.max(b,256)),c=a.Ma,a.Ma=new Uint8Array(b),0<a.Sa&&a.Ma.set(c.subarray(0,a.Sa),0))},Pb:function(a,b){if(a.Sa!=b)if(0==b)a.Ma=null,a.Sa=0;else{if(!a.Ma||a.Ma.subarray){var c=a.Ma;a.Ma=new Uint8Array(b);c&&a.Ma.set(c.subarray(0,Math.min(b,a.Sa)))}else if(a.Ma||(a.Ma=[]),a.Ma.length>b)a.Ma.length=b;else for(;a.Ma.length<b;)a.Ma.push(0);a.Sa=
|
||||
b}},Na:{Ua:function(a){var b={};b.dev=8192===(a.mode&61440)?a.id:1;b.ino=a.id;b.mode=a.mode;b.nlink=1;b.uid=0;b.gid=0;b.rdev=a.rdev;Q(a.mode)?b.size=4096:32768===(a.mode&61440)?b.size=a.Sa:40960===(a.mode&61440)?b.size=a.link.length:b.size=0;b.atime=new Date(a.timestamp);b.mtime=new Date(a.timestamp);b.ctime=new Date(a.timestamp);b.Gb=4096;b.blocks=Math.ceil(b.size/b.Gb);return b},Ta:function(a,b){void 0!==b.mode&&(a.mode=b.mode);void 0!==b.timestamp&&(a.timestamp=b.timestamp);void 0!==b.size&&P.Pb(a,
|
||||
b.size)},lookup:function(){throw Nb[44];},gb:function(a,b,c,d){return P.createNode(a,b,c,d)},rename:function(a,b,c){if(Q(a.mode)){try{var d=Ob(b,c)}catch(g){}if(d)for(var f in d.Ma)throw new O(55);}delete a.parent.Ma[a.name];a.name=c;b.Ma[c]=a;a.parent=b},unlink:function(a,b){delete a.Ma[b]},rmdir:function(a,b){var c=Ob(a,b),d;for(d in c.Ma)throw new O(55);delete a.Ma[b]},readdir:function(a){var b=[".",".."],c;for(c in a.Ma)a.Ma.hasOwnProperty(c)&&b.push(c);return b},symlink:function(a,b,c){a=P.createNode(a,
|
||||
b,41471,0);a.link=c;return a},readlink:function(a){if(40960!==(a.mode&61440))throw new O(28);return a.link}},Oa:{read:function(a,b,c,d,f){var g=a.node.Ma;if(f>=a.node.Sa)return 0;a=Math.min(a.node.Sa-f,d);if(8<a&&g.subarray)b.set(g.subarray(f,f+a),c);else for(d=0;d<a;d++)b[c+d]=g[f+d];return a},write:function(a,b,c,d,f,g){b.buffer===z.buffer&&(g=!1);if(!d)return 0;a=a.node;a.timestamp=Date.now();if(b.subarray&&(!a.Ma||a.Ma.subarray)){if(g)return a.Ma=b.subarray(c,c+d),a.Sa=d;if(0===a.Sa&&0===f)return a.Ma=
|
||||
b.slice(c,c+d),a.Sa=d;if(f+d<=a.Sa)return a.Ma.set(b.subarray(c,c+d),f),d}P.vb(a,f+d);if(a.Ma.subarray&&b.subarray)a.Ma.set(b.subarray(c,c+d),f);else for(g=0;g<d;g++)a.Ma[f+g]=b[c+g];a.Sa=Math.max(a.Sa,f+d);return d},Za:function(a,b,c){1===c?b+=a.position:2===c&&32768===(a.node.mode&61440)&&(b+=a.node.Sa);if(0>b)throw new O(28);return b},sb:function(a,b,c){P.vb(a.node,b+c);a.node.Sa=Math.max(a.node.Sa,b+c)},hb:function(a,b,c,d,f,g){assert(0===b);if(32768!==(a.node.mode&61440))throw new O(43);a=a.node.Ma;
|
||||
if(g&2||a.buffer!==Xa){if(0<d||d+c<a.length)a.subarray?a=a.subarray(d,d+c):a=Array.prototype.slice.call(a,d,d+c);d=!0;g=16384*Math.ceil(c/16384);for(b=ba(g);c<g;)z[b+c++]=0;c=b;if(!c)throw new O(48);z.set(a,c)}else d=!1,c=a.byteOffset;return{Ob:c,kb:d}},ib:function(a,b,c,d,f){if(32768!==(a.node.mode&61440))throw new O(43);if(f&2)return 0;P.Oa.write(a,b,0,d,c,!1);return 0}}},Pb=null,Qb={},S=[],Rb=1,T=null,Sb=!0,U={},O=null,Nb={};
|
||||
function V(a,b){a=Eb("/",a);b=b||{};if(!a)return{path:"",node:null};var c={wb:!0,rb:0},d;for(d in c)void 0===b[d]&&(b[d]=c[d]);if(8<b.rb)throw new O(32);a=yb(a.split("/").filter(function(n){return!!n}),!1);var f=Pb;c="/";for(d=0;d<a.length;d++){var g=d===a.length-1;if(g&&b.parent)break;f=Ob(f,a[d]);c=r(c+"/"+a[d]);f.ab&&(!g||g&&b.wb)&&(f=f.ab.root);if(!g||b.Ya)for(g=0;40960===(f.mode&61440);)if(f=Tb(c),c=Eb(zb(c),f),f=V(c,{rb:b.rb}).node,40<g++)throw new O(32);}return{path:c,node:f}}
|
||||
function Ub(a){for(var b;;){if(a===a.parent)return a=a.Wa.yb,b?"/"!==a[a.length-1]?a+"/"+b:a+b:a;b=b?a.name+"/"+b:a.name;a=a.parent}}function Vb(a,b){for(var c=0,d=0;d<b.length;d++)c=(c<<5)-c+b.charCodeAt(d)|0;return(a+c>>>0)%T.length}function Wb(a){var b=Vb(a.parent.id,a.name);if(T[b]===a)T[b]=a.bb;else for(b=T[b];b;){if(b.bb===a){b.bb=a.bb;break}b=b.bb}}
|
||||
function Ob(a,b){var c;if(c=(c=Xb(a,"x"))?c:a.Na.lookup?0:2)throw new O(c,a);for(c=T[Vb(a.id,b)];c;c=c.bb){var d=c.name;if(c.parent.id===a.id&&d===b)return c}return a.Na.lookup(a,b)}function Mb(a,b,c,d){a=new Yb(a,b,c,d);b=Vb(a.parent.id,a.name);a.bb=T[b];return T[b]=a}function Q(a){return 16384===(a&61440)}var Zb={r:0,rs:1052672,"r+":2,w:577,wx:705,xw:705,"w+":578,"wx+":706,"xw+":706,a:1089,ax:1217,xa:1217,"a+":1090,"ax+":1218,"xa+":1218};
|
||||
function $b(a){var b=["r","w","rw"][a&3];a&512&&(b+="w");return b}function Xb(a,b){if(Sb)return 0;if(-1===b.indexOf("r")||a.mode&292){if(-1!==b.indexOf("w")&&!(a.mode&146)||-1!==b.indexOf("x")&&!(a.mode&73))return 2}else return 2;return 0}function ac(a,b){try{return Ob(a,b),20}catch(c){}return Xb(a,"wx")}function bc(a,b,c){try{var d=Ob(a,b)}catch(f){return f.Pa}if(a=Xb(a,"wx"))return a;if(c){if(!Q(d.mode))return 54;if(d===d.parent||"/"===Ub(d))return 10}else if(Q(d.mode))return 31;return 0}
|
||||
function cc(a){var b=4096;for(a=a||0;a<=b;a++)if(!S[a])return a;throw new O(33);}function dc(a,b){ec||(ec=function(){},ec.prototype={});var c=new ec,d;for(d in a)c[d]=a[d];a=c;b=cc(b);a.fd=b;return S[b]=a}var Lb={open:function(a){a.Oa=Qb[a.node.rdev].Oa;a.Oa.open&&a.Oa.open(a)},Za:function(){throw new O(70);}};function Hb(a,b){Qb[a]={Oa:b}}
|
||||
function fc(a,b){var c="/"===b,d=!b;if(c&&Pb)throw new O(10);if(!c&&!d){var f=V(b,{wb:!1});b=f.path;f=f.node;if(f.ab)throw new O(10);if(!Q(f.mode))throw new O(54);}b={type:a,Ub:{},yb:b,Mb:[]};a=a.Wa(b);a.Wa=b;b.root=a;c?Pb=a:f&&(f.ab=b,f.Wa&&f.Wa.Mb.push(b))}function da(a,b,c){var d=V(a,{parent:!0}).node;a=Ab(a);if(!a||"."===a||".."===a)throw new O(28);var f=ac(d,a);if(f)throw new O(f);if(!d.Na.gb)throw new O(63);return d.Na.gb(d,a,b,c)}function W(a,b){da(a,(void 0!==b?b:511)&1023|16384,0)}
|
||||
function hc(a,b,c){"undefined"===typeof c&&(c=b,b=438);da(a,b|8192,c)}function ic(a,b){if(!Eb(a))throw new O(44);var c=V(b,{parent:!0}).node;if(!c)throw new O(44);b=Ab(b);var d=ac(c,b);if(d)throw new O(d);if(!c.Na.symlink)throw new O(63);c.Na.symlink(c,b,a)}
|
||||
function ta(a){var b=V(a,{parent:!0}).node,c=Ab(a),d=Ob(b,c),f=bc(b,c,!1);if(f)throw new O(f);if(!b.Na.unlink)throw new O(63);if(d.ab)throw new O(10);try{U.willDeletePath&&U.willDeletePath(a)}catch(g){J("FS.trackingDelegate['willDeletePath']('"+a+"') threw an exception: "+g.message)}b.Na.unlink(b,c);Wb(d);try{if(U.onDeletePath)U.onDeletePath(a)}catch(g){J("FS.trackingDelegate['onDeletePath']('"+a+"') threw an exception: "+g.message)}}
|
||||
function Tb(a){a=V(a).node;if(!a)throw new O(44);if(!a.Na.readlink)throw new O(28);return Eb(Ub(a.parent),a.Na.readlink(a))}function jc(a,b){a=V(a,{Ya:!b}).node;if(!a)throw new O(44);if(!a.Na.Ua)throw new O(63);return a.Na.Ua(a)}function kc(a){return jc(a,!0)}function ea(a,b){var c;"string"===typeof a?c=V(a,{Ya:!0}).node:c=a;if(!c.Na.Ta)throw new O(63);c.Na.Ta(c,{mode:b&4095|c.mode&-4096,timestamp:Date.now()})}
|
||||
function lc(a){var b;"string"===typeof a?b=V(a,{Ya:!0}).node:b=a;if(!b.Na.Ta)throw new O(63);b.Na.Ta(b,{timestamp:Date.now()})}function mc(a,b){if(0>b)throw new O(28);var c;"string"===typeof a?c=V(a,{Ya:!0}).node:c=a;if(!c.Na.Ta)throw new O(63);if(Q(c.mode))throw new O(31);if(32768!==(c.mode&61440))throw new O(28);if(a=Xb(c,"w"))throw new O(a);c.Na.Ta(c,{size:b,timestamp:Date.now()})}
|
||||
function u(a,b,c,d){if(""===a)throw new O(44);if("string"===typeof b){var f=Zb[b];if("undefined"===typeof f)throw Error("Unknown file open mode: "+b);b=f}c=b&64?("undefined"===typeof c?438:c)&4095|32768:0;if("object"===typeof a)var g=a;else{a=r(a);try{g=V(a,{Ya:!(b&131072)}).node}catch(n){}}f=!1;if(b&64)if(g){if(b&128)throw new O(20);}else g=da(a,c,0),f=!0;if(!g)throw new O(44);8192===(g.mode&61440)&&(b&=-513);if(b&65536&&!Q(g.mode))throw new O(54);if(!f&&(c=g?40960===(g.mode&61440)?32:Q(g.mode)&&
|
||||
("r"!==$b(b)||b&512)?31:Xb(g,$b(b)):44))throw new O(c);b&512&&mc(g,0);b&=-131713;d=dc({node:g,path:Ub(g),flags:b,seekable:!0,position:0,Oa:g.Oa,Rb:[],error:!1},d);d.Oa.open&&d.Oa.open(d);!e.logReadFiles||b&1||(Pc||(Pc={}),a in Pc||(Pc[a]=1,J("FS.trackingDelegate error on read file: "+a)));try{U.onOpenFile&&(g=0,1!==(b&2097155)&&(g|=1),0!==(b&2097155)&&(g|=2),U.onOpenFile(a,g))}catch(n){J("FS.trackingDelegate['onOpenFile']('"+a+"', flags) threw an exception: "+n.message)}return d}
|
||||
function ka(a){if(null===a.fd)throw new O(8);a.ob&&(a.ob=null);try{a.Oa.close&&a.Oa.close(a)}catch(b){throw b;}finally{S[a.fd]=null}a.fd=null}function Qc(a,b,c){if(null===a.fd)throw new O(8);if(!a.seekable||!a.Oa.Za)throw new O(70);if(0!=c&&1!=c&&2!=c)throw new O(28);a.position=a.Oa.Za(a,b,c);a.Rb=[]}
|
||||
function Sc(a,b,c,d,f){if(0>d||0>f)throw new O(28);if(null===a.fd)throw new O(8);if(1===(a.flags&2097155))throw new O(8);if(Q(a.node.mode))throw new O(31);if(!a.Oa.read)throw new O(28);var g="undefined"!==typeof f;if(!g)f=a.position;else if(!a.seekable)throw new O(70);b=a.Oa.read(a,b,c,d,f);g||(a.position+=b);return b}
|
||||
function fa(a,b,c,d,f,g){if(0>d||0>f)throw new O(28);if(null===a.fd)throw new O(8);if(0===(a.flags&2097155))throw new O(8);if(Q(a.node.mode))throw new O(31);if(!a.Oa.write)throw new O(28);a.seekable&&a.flags&1024&&Qc(a,0,2);var n="undefined"!==typeof f;if(!n)f=a.position;else if(!a.seekable)throw new O(70);b=a.Oa.write(a,b,c,d,f,g);n||(a.position+=b);try{if(a.path&&U.onWriteToFile)U.onWriteToFile(a.path)}catch(t){J("FS.trackingDelegate['onWriteToFile']('"+a.path+"') threw an exception: "+t.message)}return b}
|
||||
function sa(a){var b={encoding:"binary"};b=b||{};b.flags=b.flags||"r";b.encoding=b.encoding||"binary";if("utf8"!==b.encoding&&"binary"!==b.encoding)throw Error('Invalid encoding type "'+b.encoding+'"');var c,d=u(a,b.flags);a=jc(a).size;var f=new Uint8Array(a);Sc(d,f,0,a,0);"utf8"===b.encoding?c=Va(f,0):"binary"===b.encoding&&(c=f);ka(d);return c}
|
||||
function Tc(){O||(O=function(a,b){this.node=b;this.Qb=function(c){this.Pa=c};this.Qb(a);this.message="FS error"},O.prototype=Error(),O.prototype.constructor=O,[44].forEach(function(a){Nb[a]=new O(a);Nb[a].stack="<generic error, no stack>"}))}var Uc;function ca(a,b){var c=0;a&&(c|=365);b&&(c|=146);return c}
|
||||
function Vc(a,b,c){a=r("/dev/"+a);var d=ca(!!b,!!c);Wc||(Wc=64);var f=Wc++<<8|0;Hb(f,{open:function(g){g.seekable=!1},close:function(){c&&c.buffer&&c.buffer.length&&c(10)},read:function(g,n,t,w){for(var v=0,C=0;C<w;C++){try{var H=b()}catch(Y){throw new O(29);}if(void 0===H&&0===v)throw new O(6);if(null===H||void 0===H)break;v++;n[t+C]=H}v&&(g.node.timestamp=Date.now());return v},write:function(g,n,t,w){for(var v=0;v<w;v++)try{c(n[t+v])}catch(C){throw new O(29);}w&&(g.node.timestamp=Date.now());return v}});
|
||||
hc(a,d,f)}var Wc,X={},ec,Pc,Xc={};
|
||||
function Yc(a,b,c){try{var d=a(b)}catch(f){if(f&&f.node&&r(b)!==r(Ub(f.node)))return-54;throw f;}L[c>>2]=d.dev;L[c+4>>2]=0;L[c+8>>2]=d.ino;L[c+12>>2]=d.mode;L[c+16>>2]=d.nlink;L[c+20>>2]=d.uid;L[c+24>>2]=d.gid;L[c+28>>2]=d.rdev;L[c+32>>2]=0;M=[d.size>>>0,(N=d.size,1<=+Math.abs(N)?0<N?(Math.min(+Math.floor(N/4294967296),4294967295)|0)>>>0:~~+Math.ceil((N-+(~~N>>>0))/4294967296)>>>0:0)];L[c+40>>2]=M[0];L[c+44>>2]=M[1];L[c+48>>2]=4096;L[c+52>>2]=d.blocks;L[c+56>>2]=d.atime.getTime()/1E3|0;L[c+60>>2]=
|
||||
0;L[c+64>>2]=d.mtime.getTime()/1E3|0;L[c+68>>2]=0;L[c+72>>2]=d.ctime.getTime()/1E3|0;L[c+76>>2]=0;M=[d.ino>>>0,(N=d.ino,1<=+Math.abs(N)?0<N?(Math.min(+Math.floor(N/4294967296),4294967295)|0)>>>0:~~+Math.ceil((N-+(~~N>>>0))/4294967296)>>>0:0)];L[c+80>>2]=M[0];L[c+84>>2]=M[1];return 0}var Zc=void 0;function $c(){Zc+=4;return L[Zc-4>>2]}function Z(a){a=S[a];if(!a)throw new O(8);return a}var ad={};
|
||||
function bd(){if(!cd){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"===typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:xa||"./this.program"},b;for(b in ad)a[b]=ad[b];var c=[];for(b in a)c.push(b+"="+a[b]);cd=c}return cd}var cd,dd;za?dd=function(){var a=process.hrtime();return 1E3*a[0]+a[1]/1E6}:"undefined"!==typeof dateNow?dd=dateNow:dd=function(){return performance.now()};
|
||||
function ed(a){for(var b=dd();dd()-b<a/1E3;);}e._usleep=ed;function Yb(a,b,c,d){a||(a=this);this.parent=a;this.Wa=a.Wa;this.ab=null;this.id=Rb++;this.name=b;this.mode=c;this.Na={};this.Oa={};this.rdev=d}Object.defineProperties(Yb.prototype,{read:{get:function(){return 365===(this.mode&365)},set:function(a){a?this.mode|=365:this.mode&=-366}},write:{get:function(){return 146===(this.mode&146)},set:function(a){a?this.mode|=146:this.mode&=-147}}});Tc();T=Array(4096);fc(P,"/");W("/tmp");W("/home");W("/home/web_user");
|
||||
(function(){W("/dev");Hb(259,{read:function(){return 0},write:function(b,c,d,f){return f}});hc("/dev/null",259);Gb(1280,Jb);Gb(1536,Kb);hc("/dev/tty",1280);hc("/dev/tty1",1536);var a=Db();Vc("random",a);Vc("urandom",a);W("/dev/shm");W("/dev/shm/tmp")})();W("/proc");W("/proc/self");W("/proc/self/fd");
|
||||
fc({Wa:function(){var a=Mb("/proc/self","fd",16895,73);a.Na={lookup:function(b,c){var d=S[+c];if(!d)throw new O(8);b={parent:null,Wa:{yb:"fake"},Na:{readlink:function(){return d.path}}};return b.parent=b}};return a}},"/proc/self/fd");function la(a,b){var c=Array(aa(a)+1);a=k(a,c,0,c.length);b&&(c.length=a);return c}ab.push({Hb:function(){fd()}});
|
||||
var id={b:function(a,b,c,d){K("Assertion failed: "+A(a)+", at: "+[b?A(b):"unknown filename",c,d?A(d):"unknown function"])},r:function(a,b){sb();a=new Date(1E3*L[a>>2]);L[b>>2]=a.getSeconds();L[b+4>>2]=a.getMinutes();L[b+8>>2]=a.getHours();L[b+12>>2]=a.getDate();L[b+16>>2]=a.getMonth();L[b+20>>2]=a.getFullYear()-1900;L[b+24>>2]=a.getDay();var c=new Date(a.getFullYear(),0,1);L[b+28>>2]=(a.getTime()-c.getTime())/864E5|0;L[b+36>>2]=-(60*a.getTimezoneOffset());var d=(new Date(a.getFullYear(),6,1)).getTimezoneOffset();
|
||||
c=c.getTimezoneOffset();a=(d!=c&&a.getTimezoneOffset()==Math.min(c,d))|0;L[b+32>>2]=a;a=L[xb()+(a?4:0)>>2];L[b+40>>2]=a;return b},j:function(a,b){try{a=A(a);if(b&-8)var c=-28;else{var d;(d=V(a,{Ya:!0}).node)?(a="",b&4&&(a+="r"),b&2&&(a+="w"),b&1&&(a+="x"),c=a&&Xb(d,a)?-2:0):c=-44}return c}catch(f){return"undefined"!==typeof X&&f instanceof O||K(f),-f.Pa}},v:function(a,b){try{return a=A(a),ea(a,b),0}catch(c){return"undefined"!==typeof X&&c instanceof O||K(c),-c.Pa}},D:function(a){try{return a=A(a),
|
||||
lc(a),0}catch(b){return"undefined"!==typeof X&&b instanceof O||K(b),-b.Pa}},w:function(a,b){try{var c=S[a];if(!c)throw new O(8);ea(c.node,b);return 0}catch(d){return"undefined"!==typeof X&&d instanceof O||K(d),-d.Pa}},E:function(a){try{var b=S[a];if(!b)throw new O(8);lc(b.node);return 0}catch(c){return"undefined"!==typeof X&&c instanceof O||K(c),-c.Pa}},c:function(a,b,c){Zc=c;try{var d=Z(a);switch(b){case 0:var f=$c();return 0>f?-28:u(d.path,d.flags,0,f).fd;case 1:case 2:return 0;case 3:return d.flags;
|
||||
case 4:return f=$c(),d.flags|=f,0;case 12:return f=$c(),La[f+0>>1]=2,0;case 13:case 14:return 0;case 16:case 8:return-28;case 9:return Bb(28),-1;default:return-28}}catch(g){return"undefined"!==typeof X&&g instanceof O||K(g),-g.Pa}},x:function(a,b){try{var c=Z(a);return Yc(jc,c.path,b)}catch(d){return"undefined"!==typeof X&&d instanceof O||K(d),-d.Pa}},i:function(a,b,c){try{var d=S[a];if(!d)throw new O(8);if(0===(d.flags&2097155))throw new O(28);mc(d.node,c);return 0}catch(f){return"undefined"!==typeof X&&
|
||||
f instanceof O||K(f),-f.Pa}},J:function(a,b){try{if(0===b)return-28;if(b<aa("/")+1)return-68;k("/",m,a,b);return a}catch(c){return"undefined"!==typeof X&&c instanceof O||K(c),-c.Pa}},F:function(){return 0},e:function(){return 42},u:function(a,b){try{return a=A(a),Yc(kc,a,b)}catch(c){return"undefined"!==typeof X&&c instanceof O||K(c),-c.Pa}},t:function(a,b){try{return a=A(a),a=r(a),"/"===a[a.length-1]&&(a=a.substr(0,a.length-1)),W(a,b),0}catch(c){return"undefined"!==typeof X&&c instanceof O||K(c),
|
||||
-c.Pa}},A:function(a,b,c,d,f,g){try{a:{g<<=12;var n=!1;if(0!==(d&16)&&0!==a%16384)var t=-28;else{if(0!==(d&32)){var w=gd(16384,b);if(!w){t=-48;break a}hd(w,0,b);n=!0}else{var v=S[f];if(!v){t=-8;break a}var C=g;if(0!==(c&2)&&0===(d&2)&&2!==(v.flags&2097155))throw new O(2);if(1===(v.flags&2097155))throw new O(2);if(!v.Oa.hb)throw new O(43);var H=v.Oa.hb(v,a,b,C,c,d);w=H.Ob;n=H.kb}Xc[w]={Lb:w,Kb:b,kb:n,fd:f,Nb:c,flags:d,offset:g};t=w}}return t}catch(Y){return"undefined"!==typeof X&&Y instanceof O||K(Y),
|
||||
-Y.Pa}},z:function(a,b){try{if(-1===(a|0)||0===b)var c=-28;else{var d=Xc[a];if(d&&b===d.Kb){var f=S[d.fd];if(d.Nb&2){var g=d.flags,n=d.offset,t=m.slice(a,a+b);f&&f.Oa.ib&&f.Oa.ib(f,t,n,b,g)}Xc[a]=null;d.kb&&na(d.Lb)}c=0}return c}catch(w){return"undefined"!==typeof X&&w instanceof O||K(w),-w.Pa}},y:function(a,b,c){Zc=c;try{var d=A(a),f=$c();return u(d,b,f).fd}catch(g){return"undefined"!==typeof X&&g instanceof O||K(g),-g.Pa}},G:function(a,b,c){try{var d=Z(a);return Sc(d,z,b,c)}catch(f){return"undefined"!==
|
||||
typeof X&&f instanceof O||K(f),-f.Pa}},l:function(a,b,c){try{a=A(a);if(0>=c)var d=-28;else{var f=Tb(a),g=Math.min(c,aa(f)),n=z[b+g];k(f,m,b,c+1);z[b+g]=n;d=g}return d}catch(t){return"undefined"!==typeof X&&t instanceof O||K(t),-t.Pa}},C:function(a){try{a=A(a);var b=V(a,{parent:!0}).node,c=Ab(a),d=Ob(b,c),f=bc(b,c,!0);if(f)throw new O(f);if(!b.Na.rmdir)throw new O(63);if(d.ab)throw new O(10);try{U.willDeletePath&&U.willDeletePath(a)}catch(g){J("FS.trackingDelegate['willDeletePath']('"+a+"') threw an exception: "+
|
||||
g.message)}b.Na.rmdir(b,c);Wb(d);try{if(U.onDeletePath)U.onDeletePath(a)}catch(g){J("FS.trackingDelegate['onDeletePath']('"+a+"') threw an exception: "+g.message)}return 0}catch(g){return"undefined"!==typeof X&&g instanceof O||K(g),-g.Pa}},f:function(a,b){try{return a=A(a),Yc(jc,a,b)}catch(c){return"undefined"!==typeof X&&c instanceof O||K(c),-c.Pa}},H:function(a){try{return a=A(a),ta(a),0}catch(b){return"undefined"!==typeof X&&b instanceof O||K(b),-b.Pa}},n:function(a,b,c){m.copyWithin(a,b,b+c)},
|
||||
d:function(a){a>>>=0;var b=m.length;if(2147483648<a)return!1;for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);d=Math.max(16777216,a,d);0<d%65536&&(d+=65536-d%65536);a:{try{Oa.grow(Math.min(2147483648,d)-Xa.byteLength+65535>>>16);Ya(Oa.buffer);var f=1;break a}catch(g){}f=void 0}if(f)return!0}return!1},p:function(a,b){var c=0;bd().forEach(function(d,f){var g=b+c;f=L[a+4*f>>2]=g;for(g=0;g<d.length;++g)z[f++>>0]=d.charCodeAt(g);z[f>>0]=0;c+=d.length+1});return 0},q:function(a,b){var c=
|
||||
bd();L[a>>2]=c.length;var d=0;c.forEach(function(f){d+=f.length+1});L[b>>2]=d;return 0},g:function(a){try{var b=Z(a);ka(b);return 0}catch(c){return"undefined"!==typeof X&&c instanceof O||K(c),c.Pa}},o:function(a,b){try{var c=Z(a);z[b>>0]=c.tty?2:Q(c.mode)?3:40960===(c.mode&61440)?7:4;return 0}catch(d){return"undefined"!==typeof X&&d instanceof O||K(d),d.Pa}},m:function(a,b,c,d,f){try{var g=Z(a);a=4294967296*c+(b>>>0);if(-9007199254740992>=a||9007199254740992<=a)return-61;Qc(g,a,d);M=[g.position>>>
|
||||
0,(N=g.position,1<=+Math.abs(N)?0<N?(Math.min(+Math.floor(N/4294967296),4294967295)|0)>>>0:~~+Math.ceil((N-+(~~N>>>0))/4294967296)>>>0:0)];L[f>>2]=M[0];L[f+4>>2]=M[1];g.ob&&0===a&&0===d&&(g.ob=null);return 0}catch(n){return"undefined"!==typeof X&&n instanceof O||K(n),n.Pa}},K:function(a){try{var b=Z(a);return b.Oa&&b.Oa.fsync?-b.Oa.fsync(b):0}catch(c){return"undefined"!==typeof X&&c instanceof O||K(c),c.Pa}},I:function(a,b,c,d){try{a:{for(var f=Z(a),g=a=0;g<c;g++){var n=fa(f,z,L[b+8*g>>2],L[b+(8*
|
||||
g+4)>>2],void 0);if(0>n){var t=-1;break a}a+=n}t=a}L[d>>2]=t;return 0}catch(w){return"undefined"!==typeof X&&w instanceof O||K(w),w.Pa}},h:function(a){var b=Date.now();L[a>>2]=b/1E3|0;L[a+4>>2]=b%1E3*1E3|0;return 0},a:Oa,k:function(a,b){if(0===a)return Bb(28),-1;var c=L[a>>2];a=L[a+4>>2];if(0>a||999999999<a||0>c)return Bb(28),-1;0!==b&&(L[b>>2]=0,L[b+4>>2]=0);return ed(1E6*c+a/1E3)},B:function(a){switch(a){case 30:return 16384;case 85:return 131072;case 132:case 133:case 12:case 137:case 138:case 15:case 235:case 16:case 17:case 18:case 19:case 20:case 149:case 13:case 10:case 236:case 153:case 9:case 21:case 22:case 159:case 154:case 14:case 77:case 78:case 139:case 80:case 81:case 82:case 68:case 67:case 164:case 11:case 29:case 47:case 48:case 95:case 52:case 51:case 46:case 79:return 200809;
|
||||
case 27:case 246:case 127:case 128:case 23:case 24:case 160:case 161:case 181:case 182:case 242:case 183:case 184:case 243:case 244:case 245:case 165:case 178:case 179:case 49:case 50:case 168:case 169:case 175:case 170:case 171:case 172:case 97:case 76:case 32:case 173:case 35:return-1;case 176:case 177:case 7:case 155:case 8:case 157:case 125:case 126:case 92:case 93:case 129:case 130:case 131:case 94:case 91:return 1;case 74:case 60:case 69:case 70:case 4:return 1024;case 31:case 42:case 72:return 32;
|
||||
case 87:case 26:case 33:return 2147483647;case 34:case 1:return 47839;case 38:case 36:return 99;case 43:case 37:return 2048;case 0:return 2097152;case 3:return 65536;case 28:return 32768;case 44:return 32767;case 75:return 16384;case 39:return 1E3;case 89:return 700;case 71:return 256;case 40:return 255;case 2:return 100;case 180:return 64;case 25:return 20;case 5:return 16;case 6:return 6;case 73:return 4;case 84:return"object"===typeof navigator?navigator.hardwareConcurrency||1:1}Bb(28);return-1},
|
||||
L:function(a){var b=Date.now()/1E3|0;a&&(L[a>>2]=b);return b},s:function(a,b){if(b){var c=1E3*L[b+8>>2];c+=L[b+12>>2]/1E3}else c=Date.now();a=A(a);try{b=c;var d=V(a,{Ya:!0}).node;d.Na.Ta(d,{timestamp:Math.max(b,c)});return 0}catch(f){a=f;if(!(a instanceof O)){a+=" : ";a:{d=Error();if(!d.stack){try{throw Error();}catch(g){d=g}if(!d.stack){d="(no stack trace available)";break a}}d=d.stack.toString()}e.extraStackTrace&&(d+="\n"+e.extraStackTrace());d=ob(d);throw a+d;}Bb(a.Pa);return-1}}};
|
||||
(function(){function a(f){e.asm=f.exports;Ja=e.asm.M;eb--;e.monitorRunDependencies&&e.monitorRunDependencies(eb);0==eb&&(null!==fb&&(clearInterval(fb),fb=null),gb&&(f=gb,gb=null,f()))}function b(f){a(f.instance)}function c(f){return mb().then(function(g){return WebAssembly.instantiate(g,d)}).then(f,function(g){J("failed to asynchronously prepare wasm: "+g);K(g)})}var d={a:id};eb++;e.monitorRunDependencies&&e.monitorRunDependencies(eb);if(e.instantiateWasm)try{return e.instantiateWasm(d,a)}catch(f){return J("Module.instantiateWasm callback failed with error: "+
|
||||
f),!1}(function(){if(Ka||"function"!==typeof WebAssembly.instantiateStreaming||jb()||hb("file://")||"function"!==typeof fetch)return c(b);fetch(ib,{credentials:"same-origin"}).then(function(f){return WebAssembly.instantiateStreaming(f,d).then(b,function(g){J("wasm streaming compile failed: "+g);J("falling back to ArrayBuffer instantiation");return c(b)})})})();return{}})();
|
||||
var fd=e.___wasm_call_ctors=function(){return(fd=e.___wasm_call_ctors=e.asm.N).apply(null,arguments)},hd=e._memset=function(){return(hd=e._memset=e.asm.O).apply(null,arguments)};e._sqlite3_free=function(){return(e._sqlite3_free=e.asm.P).apply(null,arguments)};var Cb=e.___errno_location=function(){return(Cb=e.___errno_location=e.asm.Q).apply(null,arguments)};e._sqlite3_finalize=function(){return(e._sqlite3_finalize=e.asm.R).apply(null,arguments)};
|
||||
e._sqlite3_reset=function(){return(e._sqlite3_reset=e.asm.S).apply(null,arguments)};e._sqlite3_clear_bindings=function(){return(e._sqlite3_clear_bindings=e.asm.T).apply(null,arguments)};e._sqlite3_value_blob=function(){return(e._sqlite3_value_blob=e.asm.U).apply(null,arguments)};e._sqlite3_value_text=function(){return(e._sqlite3_value_text=e.asm.V).apply(null,arguments)};e._sqlite3_value_bytes=function(){return(e._sqlite3_value_bytes=e.asm.W).apply(null,arguments)};
|
||||
e._sqlite3_value_double=function(){return(e._sqlite3_value_double=e.asm.X).apply(null,arguments)};e._sqlite3_value_int=function(){return(e._sqlite3_value_int=e.asm.Y).apply(null,arguments)};e._sqlite3_value_type=function(){return(e._sqlite3_value_type=e.asm.Z).apply(null,arguments)};e._sqlite3_result_blob=function(){return(e._sqlite3_result_blob=e.asm._).apply(null,arguments)};e._sqlite3_result_double=function(){return(e._sqlite3_result_double=e.asm.$).apply(null,arguments)};
|
||||
e._sqlite3_result_error=function(){return(e._sqlite3_result_error=e.asm.aa).apply(null,arguments)};e._sqlite3_result_int=function(){return(e._sqlite3_result_int=e.asm.ba).apply(null,arguments)};e._sqlite3_result_int64=function(){return(e._sqlite3_result_int64=e.asm.ca).apply(null,arguments)};e._sqlite3_result_null=function(){return(e._sqlite3_result_null=e.asm.da).apply(null,arguments)};e._sqlite3_result_text=function(){return(e._sqlite3_result_text=e.asm.ea).apply(null,arguments)};
|
||||
e._sqlite3_step=function(){return(e._sqlite3_step=e.asm.fa).apply(null,arguments)};e._sqlite3_column_count=function(){return(e._sqlite3_column_count=e.asm.ga).apply(null,arguments)};e._sqlite3_data_count=function(){return(e._sqlite3_data_count=e.asm.ha).apply(null,arguments)};e._sqlite3_column_blob=function(){return(e._sqlite3_column_blob=e.asm.ia).apply(null,arguments)};e._sqlite3_column_bytes=function(){return(e._sqlite3_column_bytes=e.asm.ja).apply(null,arguments)};
|
||||
e._sqlite3_column_double=function(){return(e._sqlite3_column_double=e.asm.ka).apply(null,arguments)};e._sqlite3_column_text=function(){return(e._sqlite3_column_text=e.asm.la).apply(null,arguments)};e._sqlite3_column_type=function(){return(e._sqlite3_column_type=e.asm.ma).apply(null,arguments)};e._sqlite3_column_name=function(){return(e._sqlite3_column_name=e.asm.na).apply(null,arguments)};e._sqlite3_bind_blob=function(){return(e._sqlite3_bind_blob=e.asm.oa).apply(null,arguments)};
|
||||
e._sqlite3_bind_double=function(){return(e._sqlite3_bind_double=e.asm.pa).apply(null,arguments)};e._sqlite3_bind_int=function(){return(e._sqlite3_bind_int=e.asm.qa).apply(null,arguments)};e._sqlite3_bind_text=function(){return(e._sqlite3_bind_text=e.asm.ra).apply(null,arguments)};e._sqlite3_bind_parameter_index=function(){return(e._sqlite3_bind_parameter_index=e.asm.sa).apply(null,arguments)};e._sqlite3_sql=function(){return(e._sqlite3_sql=e.asm.ta).apply(null,arguments)};
|
||||
e._sqlite3_normalized_sql=function(){return(e._sqlite3_normalized_sql=e.asm.ua).apply(null,arguments)};e._sqlite3_errmsg=function(){return(e._sqlite3_errmsg=e.asm.va).apply(null,arguments)};e._sqlite3_exec=function(){return(e._sqlite3_exec=e.asm.wa).apply(null,arguments)};e._sqlite3_prepare_v2=function(){return(e._sqlite3_prepare_v2=e.asm.xa).apply(null,arguments)};e._sqlite3_changes=function(){return(e._sqlite3_changes=e.asm.ya).apply(null,arguments)};
|
||||
e._sqlite3_close_v2=function(){return(e._sqlite3_close_v2=e.asm.za).apply(null,arguments)};e._sqlite3_create_function_v2=function(){return(e._sqlite3_create_function_v2=e.asm.Aa).apply(null,arguments)};e._sqlite3_open=function(){return(e._sqlite3_open=e.asm.Ba).apply(null,arguments)};var ba=e._malloc=function(){return(ba=e._malloc=e.asm.Ca).apply(null,arguments)},na=e._free=function(){return(na=e._free=e.asm.Da).apply(null,arguments)};
|
||||
e._RegisterExtensionFunctions=function(){return(e._RegisterExtensionFunctions=e.asm.Ea).apply(null,arguments)};
|
||||
var xb=e.__get_tzname=function(){return(xb=e.__get_tzname=e.asm.Fa).apply(null,arguments)},vb=e.__get_daylight=function(){return(vb=e.__get_daylight=e.asm.Ga).apply(null,arguments)},ub=e.__get_timezone=function(){return(ub=e.__get_timezone=e.asm.Ha).apply(null,arguments)},oa=e.stackSave=function(){return(oa=e.stackSave=e.asm.Ia).apply(null,arguments)},qa=e.stackRestore=function(){return(qa=e.stackRestore=e.asm.Ja).apply(null,arguments)},y=e.stackAlloc=function(){return(y=e.stackAlloc=e.asm.Ka).apply(null,
|
||||
arguments)},gd=e._memalign=function(){return(gd=e._memalign=e.asm.La).apply(null,arguments)};e.cwrap=function(a,b,c,d){c=c||[];var f=c.every(function(g){return"number"===g});return"string"!==b&&f&&!d?Qa(a):function(){return Ra(a,b,c,arguments)}};e.UTF8ToString=A;e.stackSave=oa;e.stackRestore=qa;e.stackAlloc=y;var jd;gb=function kd(){jd||ld();jd||(gb=kd)};
|
||||
function ld(){function a(){if(!jd&&(jd=!0,e.calledRun=!0,!Pa)){e.noFSInit||Uc||(Uc=!0,Tc(),e.stdin=e.stdin,e.stdout=e.stdout,e.stderr=e.stderr,e.stdin?Vc("stdin",e.stdin):ic("/dev/tty","/dev/stdin"),e.stdout?Vc("stdout",null,e.stdout):ic("/dev/tty","/dev/stdout"),e.stderr?Vc("stderr",null,e.stderr):ic("/dev/tty1","/dev/stderr"),u("/dev/stdin","r"),u("/dev/stdout","w"),u("/dev/stderr","w"));nb(ab);Sb=!1;nb(bb);if(e.onRuntimeInitialized)e.onRuntimeInitialized();if(e.postRun)for("function"==typeof e.postRun&&
|
||||
(e.postRun=[e.postRun]);e.postRun.length;){var b=e.postRun.shift();cb.unshift(b)}nb(cb)}}if(!(0<eb)){if(e.preRun)for("function"==typeof e.preRun&&(e.preRun=[e.preRun]);e.preRun.length;)db();nb($a);0<eb||(e.setStatus?(e.setStatus("Running..."),setTimeout(function(){setTimeout(function(){e.setStatus("")},1);a()},1)):a())}}e.run=ld;if(e.preInit)for("function"==typeof e.preInit&&(e.preInit=[e.preInit]);0<e.preInit.length;)e.preInit.pop()();noExitRuntime=!0;ld();
|
||||
|
||||
|
||||
// The shell-pre.js and emcc-generated code goes above
|
||||
return Module;
|
||||
}); // The end of the promise being returned
|
||||
|
||||
return initSqlJsPromise;
|
||||
} // The end of our initSqlJs function
|
||||
|
||||
// This bit below is copied almost exactly from what you get when you use the MODULARIZE=1 flag with emcc
|
||||
// However, we don't want to use the emcc modularization. See shell-pre.js
|
||||
if (typeof exports === 'object' && typeof module === 'object'){
|
||||
module.exports = initSqlJs;
|
||||
// This will allow the module to be used in ES6 or CommonJS
|
||||
module.exports.default = initSqlJs;
|
||||
}
|
||||
else if (typeof define === 'function' && define['amd']) {
|
||||
define([], function() { return initSqlJs; });
|
||||
}
|
||||
else if (typeof exports === 'object'){
|
||||
exports["Module"] = initSqlJs;
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -1,5 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
@@ -43,10 +44,10 @@ class FileAttachment extends AttachmentWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
child: _getFileTypeImage(context),
|
||||
height: 40.0,
|
||||
width: 33.33,
|
||||
margin: EdgeInsets.all(8.0),
|
||||
child: _getFileTypeImage(context),
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Expanded(
|
||||
@@ -102,12 +103,15 @@ class FileAttachment extends AttachmentWidget {
|
||||
errorWidget: (_, obj, trace) {
|
||||
return getFileTypeImage(attachment.extraData['other']);
|
||||
},
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
placeholder: (_, __) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import '../full_screen_media.dart';
|
||||
@@ -139,6 +140,7 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
Card(
|
||||
color: Colors.white,
|
||||
elevation: 2,
|
||||
shape: CircleBorder(),
|
||||
child: IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tight(Size(32, 32)),
|
||||
@@ -152,7 +154,6 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
});
|
||||
},
|
||||
),
|
||||
shape: CircleBorder(),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
@@ -167,6 +168,7 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
Card(
|
||||
color: Colors.white,
|
||||
elevation: 2,
|
||||
shape: CircleBorder(),
|
||||
child: IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tight(Size(32, 32)),
|
||||
@@ -180,7 +182,6 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
});
|
||||
},
|
||||
),
|
||||
shape: CircleBorder(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -345,11 +346,15 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) {
|
||||
return Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
return Shimmer.fromColors(
|
||||
baseColor:
|
||||
StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
@@ -51,22 +52,46 @@ class ImageAttachment extends AttachmentWidget {
|
||||
);
|
||||
},
|
||||
network: () {
|
||||
final imageUrl =
|
||||
var imageUrl =
|
||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
||||
|
||||
if (imageUrl == null) {
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
|
||||
var imageUri = Uri.parse(imageUrl);
|
||||
if (imageUri.host == 'stream-io-cdn.com') {
|
||||
imageUri = imageUri.replace(queryParameters: {
|
||||
...imageUri.queryParameters,
|
||||
'h': '500',
|
||||
'w': '500',
|
||||
'crop': 'center',
|
||||
'resize': 'crop',
|
||||
});
|
||||
} else if (imageUri.host == 'stream-cloud-uploads.imgix.net') {
|
||||
imageUri = imageUri.replace(queryParameters: {
|
||||
...imageUri.queryParameters,
|
||||
'height': '500',
|
||||
'width': '500',
|
||||
'fit': 'crop',
|
||||
});
|
||||
}
|
||||
imageUrl = imageUri.toString();
|
||||
|
||||
return _buildImageAttachment(
|
||||
context,
|
||||
CachedNetworkImage(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) {
|
||||
return Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
return Shimmer.fromColors(
|
||||
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ class VideoAttachment extends AttachmentWidget {
|
||||
final MessageTheme messageTheme;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
final VoidCallback onAttachmentTap;
|
||||
|
||||
const VideoAttachment({
|
||||
Key key,
|
||||
@@ -20,6 +21,7 @@ class VideoAttachment extends AttachmentWidget {
|
||||
this.messageTheme,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
this.onAttachmentTap,
|
||||
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||
|
||||
@override
|
||||
@@ -65,25 +67,26 @@ class VideoAttachment extends AttachmentWidget {
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final res = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
onTap: onAttachmentTap ??
|
||||
() async {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final res = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (res != null) onReturnAction(res);
|
||||
},
|
||||
);
|
||||
if (res != null) onReturnAction(res);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
videoWidget,
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'extension.dart';
|
||||
|
||||
/// Callback to download an attachment asset
|
||||
typedef AttachmentDownloader = Future<String> Function(
|
||||
Attachment attachment, {
|
||||
ProgressCallback progressCallback,
|
||||
});
|
||||
|
||||
/// Widget that shows the options in the gallery view
|
||||
class AttachmentActionsModal extends StatelessWidget {
|
||||
/// The message containing the attachments
|
||||
final Message message;
|
||||
|
||||
/// Current page index
|
||||
final currentIndex;
|
||||
|
||||
/// Callback to show the message
|
||||
final VoidCallback onShowMessage;
|
||||
|
||||
/// Callback to download images
|
||||
final AttachmentDownloader imageDownloader;
|
||||
|
||||
/// Callback to provide download files
|
||||
final AttachmentDownloader fileDownloader;
|
||||
|
||||
/// Returns a new [AttachmentActionsModal]
|
||||
const AttachmentActionsModal({
|
||||
this.message,
|
||||
this.currentIndex,
|
||||
this.onShowMessage,
|
||||
this.imageDownloader,
|
||||
this.fileDownloader,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Navigator.maybePop(context),
|
||||
child: _buildPage(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPage(context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(height: kToolbarHeight),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Container(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildButton(
|
||||
context,
|
||||
'Reply',
|
||||
StreamSvgIcon.iconCurveLineLeftUp(
|
||||
size: 24.0,
|
||||
color: theme.colorTheme.grey,
|
||||
),
|
||||
() {
|
||||
Navigator.pop(context, ReturnActionType.reply);
|
||||
},
|
||||
),
|
||||
_buildButton(
|
||||
context,
|
||||
'Show in Chat',
|
||||
StreamSvgIcon.eye(
|
||||
size: 24.0,
|
||||
color: theme.colorTheme.black,
|
||||
),
|
||||
onShowMessage,
|
||||
),
|
||||
_buildButton(
|
||||
context,
|
||||
'Save ${message.attachments[currentIndex].type == 'video' ? 'Video' : 'Image'}',
|
||||
StreamSvgIcon.iconSave(
|
||||
size: 24.0,
|
||||
color: theme.colorTheme.grey,
|
||||
),
|
||||
() {
|
||||
final attachment = message.attachments[currentIndex];
|
||||
final isImage = attachment.type == 'image';
|
||||
final saveFile = fileDownloader ?? _downloadAttachment;
|
||||
final saveImage = imageDownloader ?? _downloadAttachment;
|
||||
final downloader = isImage ? saveImage : saveFile;
|
||||
|
||||
final progressNotifier = ValueNotifier<_DownloadProgress>(
|
||||
_DownloadProgress.initial(),
|
||||
);
|
||||
|
||||
downloader(
|
||||
attachment,
|
||||
progressCallback: (received, total) {
|
||||
progressNotifier.value = _DownloadProgress(
|
||||
total,
|
||||
received,
|
||||
);
|
||||
},
|
||||
).catchError((_) {
|
||||
progressNotifier.value = null;
|
||||
});
|
||||
|
||||
// Closing attachment actions modal before opening
|
||||
// attachment download dialog
|
||||
Navigator.pop(context);
|
||||
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
barrierColor: theme.colorTheme.overlay,
|
||||
builder: (context) => _buildDownloadProgressDialog(
|
||||
context,
|
||||
progressNotifier,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (StreamChat.of(context).user.id == message.user.id)
|
||||
_buildButton(
|
||||
context,
|
||||
'Delete',
|
||||
StreamSvgIcon.delete(
|
||||
size: 24.0,
|
||||
color: theme.colorTheme.accentRed,
|
||||
),
|
||||
() {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (message.attachments.length > 1 ||
|
||||
message.text.isNotEmpty) {
|
||||
final remainingAttachments = [...message.attachments]
|
||||
..removeAt(currentIndex);
|
||||
channel.updateMessage(message.copyWith(
|
||||
attachments: remainingAttachments,
|
||||
));
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
channel.deleteMessage(message).then((value) {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
});
|
||||
}
|
||||
},
|
||||
color: theme.colorTheme.accentRed,
|
||||
),
|
||||
]
|
||||
.map<Widget>((e) =>
|
||||
Align(alignment: Alignment.centerRight, child: e))
|
||||
.insertBetween(
|
||||
Container(
|
||||
height: 1,
|
||||
color: theme.colorTheme.greyWhisper,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButton(
|
||||
context,
|
||||
String title,
|
||||
StreamSvgIcon icon,
|
||||
VoidCallback onTap, {
|
||||
Color color,
|
||||
}) {
|
||||
return Material(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
icon,
|
||||
SizedBox(width: 16),
|
||||
Text(
|
||||
title,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDownloadProgressDialog(
|
||||
BuildContext context,
|
||||
ValueNotifier<_DownloadProgress> progressNotifier,
|
||||
) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () => Future.value(false),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: progressNotifier,
|
||||
builder: (_, _DownloadProgress progress, __) {
|
||||
// Pop the dialog in case the progress is null or it's completed.
|
||||
if (progress == null || progress?.toProgressIndicatorValue == 1.0) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
Navigator.of(context).pop,
|
||||
);
|
||||
}
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: 182,
|
||||
width: 182,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: theme.colorTheme.white,
|
||||
),
|
||||
child: Center(
|
||||
child: progress == null
|
||||
? Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: StreamSvgIcon.error(
|
||||
color: theme.colorTheme.greyGainsboro,
|
||||
),
|
||||
)
|
||||
: progress.toProgressIndicatorValue == 1.0
|
||||
? Container(
|
||||
height: 160,
|
||||
width: 160,
|
||||
child: StreamSvgIcon.check(
|
||||
color: theme.colorTheme.greyGainsboro,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: progress.toProgressIndicatorValue,
|
||||
strokeWidth: 8.0,
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
theme.colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
'${progress.toPercentage}%',
|
||||
style: theme.textTheme.headline.copyWith(
|
||||
color: theme.colorTheme.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _downloadAttachment(
|
||||
Attachment attachment, {
|
||||
ProgressCallback progressCallback,
|
||||
}) async {
|
||||
String filePath;
|
||||
final appDocDir = await getTemporaryDirectory();
|
||||
await Dio().download(
|
||||
attachment.assetUrl ?? attachment.imageUrl ?? attachment.thumbUrl,
|
||||
(Headers responseHeaders) {
|
||||
final contentType = responseHeaders[Headers.contentTypeHeader];
|
||||
final mimeType = contentType.first?.split('/')?.last;
|
||||
filePath ??= '${appDocDir.path}/${attachment.id}.$mimeType';
|
||||
return filePath;
|
||||
},
|
||||
onReceiveProgress: progressCallback,
|
||||
);
|
||||
final result = await ImageGallerySaver.saveFile(filePath);
|
||||
return (result as Map)['filePath'];
|
||||
}
|
||||
}
|
||||
|
||||
class _DownloadProgress {
|
||||
final int total;
|
||||
final int received;
|
||||
|
||||
const _DownloadProgress(this.total, this.received);
|
||||
|
||||
factory _DownloadProgress.initial() {
|
||||
return _DownloadProgress(double.maxFinite.toInt(), 0);
|
||||
}
|
||||
|
||||
double get toProgressIndicatorValue => received / total;
|
||||
|
||||
int get toPercentage => (received * 100) ~/ total;
|
||||
}
|
||||
@@ -21,28 +21,28 @@ class StreamBackButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: () {
|
||||
if (onPressed != null) {
|
||||
onPressed();
|
||||
} else {
|
||||
Navigator.maybePop(context);
|
||||
}
|
||||
},
|
||||
padding: const EdgeInsets.all(14.0),
|
||||
child: RawMaterialButton(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: () {
|
||||
if (onPressed != null) {
|
||||
onPressed();
|
||||
} else {
|
||||
Navigator.maybePop(context);
|
||||
}
|
||||
},
|
||||
child: StreamSvgIcon.left(
|
||||
size: 24,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
child: StreamSvgIcon.left(
|
||||
size: 24,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
if (showUnreads)
|
||||
|
||||
@@ -114,9 +114,9 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
children: [
|
||||
UserAvatar(
|
||||
user: members[index].user,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 64.0,
|
||||
maxWidth: 64.0,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 64.0,
|
||||
width: 64.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
onlineIndicatorConstraints:
|
||||
@@ -238,8 +238,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
);
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
if (res == true) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
await channel.removeMembers([StreamChat.of(context).user.id]);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@@ -71,12 +71,12 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
child: StreamSvgIcon.left(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -71,6 +71,19 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
final bool showConnectionStateTile;
|
||||
|
||||
/// Title widget
|
||||
final Widget title;
|
||||
|
||||
/// Subtitle widget
|
||||
final Widget subtitle;
|
||||
|
||||
/// Leading widget
|
||||
final Widget leading;
|
||||
|
||||
/// AppBar actions
|
||||
/// By default it shows the [ChannelImage]
|
||||
final List<Widget> actions;
|
||||
|
||||
/// Creates a channel header
|
||||
ChannelHeader({
|
||||
Key key,
|
||||
@@ -80,6 +93,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
this.showTypingIndicator = true,
|
||||
this.onImageTap,
|
||||
this.showConnectionStateTile = false,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.actions,
|
||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
|
||||
@@ -87,6 +104,14 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
final leadingWidget = leading ??
|
||||
(showBackButton
|
||||
? StreamBackButton(
|
||||
onPressed: onBackPressed,
|
||||
showUnreads: true,
|
||||
)
|
||||
: SizedBox());
|
||||
|
||||
return ConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
var statusString = '';
|
||||
@@ -111,26 +136,32 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
child: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
leading: showBackButton
|
||||
? StreamBackButton(
|
||||
onPressed: onBackPressed,
|
||||
showUnreads: true,
|
||||
)
|
||||
: SizedBox(),
|
||||
leading: leadingWidget,
|
||||
backgroundColor: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.color,
|
||||
actions: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Center(
|
||||
child: ChannelImage(
|
||||
onTap: onImageTap,
|
||||
actions: actions ??
|
||||
<Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Center(
|
||||
child: ChannelImage(
|
||||
borderRadius: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
constraints: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
onTap: onImageTap,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
centerTitle: true,
|
||||
title: InkWell(
|
||||
onTap: onTitleTap,
|
||||
@@ -141,20 +172,23 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ChannelName(
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title,
|
||||
),
|
||||
title ??
|
||||
ChannelName(
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title,
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
ChannelInfo(
|
||||
showTypingIndicator: showTypingIndicator,
|
||||
channel: channel,
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle,
|
||||
),
|
||||
subtitle ??
|
||||
ChannelInfo(
|
||||
showTypingIndicator: showTypingIndicator,
|
||||
channel: channel,
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -96,7 +96,11 @@ class ChannelImage extends StatelessWidget {
|
||||
initialData: otherMember.user,
|
||||
builder: (context, snapshot) {
|
||||
return UserAvatar(
|
||||
borderRadius: borderRadius,
|
||||
borderRadius: borderRadius ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
user: snapshot.data ?? otherMember.user,
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
@@ -120,7 +124,11 @@ class ChannelImage extends StatelessWidget {
|
||||
.toList();
|
||||
return GroupImage(
|
||||
images: images,
|
||||
borderRadius: borderRadius,
|
||||
borderRadius: borderRadius ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
|
||||
@@ -57,7 +57,7 @@ class ChannelInfo extends StatelessWidget {
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
.subtitle,
|
||||
);
|
||||
} else {
|
||||
final otherMember = members.firstWhere(
|
||||
@@ -69,10 +69,7 @@ class ChannelInfo extends StatelessWidget {
|
||||
if (otherMember.user.online) {
|
||||
alternativeWidget = Text(
|
||||
'Online',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
style: textStyle,
|
||||
);
|
||||
} else {
|
||||
alternativeWidget = Text(
|
||||
|
||||
@@ -45,7 +45,7 @@ typedef _TitleBuilder = Widget Function(
|
||||
/// The widget by default uses the inherited [StreamChatClient] to fetch information about the status.
|
||||
/// However you can also pass your own [StreamChatClient] if you don't have it in the widget tree.
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelListHeaderTheme] property.
|
||||
/// Modify it to change the widget appearance.
|
||||
class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// Instantiates a ChannelListHeader
|
||||
@@ -57,6 +57,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
this.onNewChatButtonTap,
|
||||
this.showConnectionStateTile = false,
|
||||
this.preNavigationCallback,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.actions,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Pass this if you don't have a [StreamChatClient] in your widget tree.
|
||||
@@ -76,6 +79,17 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
final VoidCallback preNavigationCallback;
|
||||
|
||||
/// Subtitle widget
|
||||
final Widget subtitle;
|
||||
|
||||
/// Leading widget
|
||||
/// By default it shows the logged in user avatar
|
||||
final Widget leading;
|
||||
|
||||
/// AppBar actions
|
||||
/// By default it shows the new chat button
|
||||
final List<Widget> actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _client = client ?? StreamChat.of(context).client;
|
||||
@@ -104,76 +118,85 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
child: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
backgroundColor: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.color,
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).channelListHeaderTheme.color,
|
||||
centerTitle: true,
|
||||
leading: Center(
|
||||
child: UserAvatar(
|
||||
user: user,
|
||||
showOnlineStatus: false,
|
||||
onTap: onUserAvatarTap ??
|
||||
(_) {
|
||||
if (preNavigationCallback != null) {
|
||||
preNavigationCallback();
|
||||
}
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
StreamNeumorphicButton(
|
||||
child: IconButton(
|
||||
icon: ConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
var color;
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
color =
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue;
|
||||
break;
|
||||
case ConnectionStatus.connecting:
|
||||
color = Colors.grey;
|
||||
break;
|
||||
case ConnectionStatus.disconnected:
|
||||
color = Colors.grey;
|
||||
break;
|
||||
}
|
||||
return SvgPicture.asset(
|
||||
'svgs/icon_pen_write.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
color: color,
|
||||
);
|
||||
},
|
||||
leading: leading ??
|
||||
Center(
|
||||
child: UserAvatar(
|
||||
user: user,
|
||||
showOnlineStatus: false,
|
||||
onTap: onUserAvatarTap ??
|
||||
(_) {
|
||||
if (preNavigationCallback != null) {
|
||||
preNavigationCallback();
|
||||
}
|
||||
Scaffold.of(context).openDrawer();
|
||||
},
|
||||
borderRadius: StreamChatTheme.of(context)
|
||||
.channelListHeaderTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
constraints: StreamChatTheme.of(context)
|
||||
.channelListHeaderTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
),
|
||||
onPressed: onNewChatButtonTap,
|
||||
),
|
||||
)
|
||||
],
|
||||
title: Builder(
|
||||
builder: (context) {
|
||||
if (titleBuilder != null) {
|
||||
return titleBuilder(context, status, _client);
|
||||
}
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
return _buildConnectedTitleState(context);
|
||||
case ConnectionStatus.connecting:
|
||||
return _buildConnectingTitleState(context);
|
||||
case ConnectionStatus.disconnected:
|
||||
return _buildDisconnectedTitleState(context, _client);
|
||||
default:
|
||||
return Offstage();
|
||||
}
|
||||
},
|
||||
actions: actions ??
|
||||
[
|
||||
StreamNeumorphicButton(
|
||||
child: IconButton(
|
||||
icon: ConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
var color;
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
color = StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue;
|
||||
break;
|
||||
case ConnectionStatus.connecting:
|
||||
color = Colors.grey;
|
||||
break;
|
||||
case ConnectionStatus.disconnected:
|
||||
color = Colors.grey;
|
||||
break;
|
||||
}
|
||||
return SvgPicture.asset(
|
||||
'svgs/icon_pen_write.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
color: color,
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: onNewChatButtonTap,
|
||||
),
|
||||
)
|
||||
],
|
||||
title: Column(
|
||||
children: [
|
||||
Builder(
|
||||
builder: (context) {
|
||||
if (titleBuilder != null) {
|
||||
return titleBuilder(context, status, _client);
|
||||
}
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
return _buildConnectedTitleState(context);
|
||||
case ConnectionStatus.connecting:
|
||||
return _buildConnectingTitleState(context);
|
||||
case ConnectionStatus.disconnected:
|
||||
return _buildDisconnectedTitleState(context, _client);
|
||||
default:
|
||||
return Offstage();
|
||||
}
|
||||
},
|
||||
),
|
||||
subtitle ?? Offstage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -202,14 +225,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'Searching for Network',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style:
|
||||
StreamChatTheme.of(context).channelListHeaderTheme.title.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -222,14 +242,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
children: [
|
||||
Text(
|
||||
'Offline...',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style:
|
||||
StreamChatTheme.of(context).channelListHeaderTheme.title.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
@@ -239,8 +256,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
child: Text(
|
||||
'Try Again',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.channelListHeaderTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontSize: 16,
|
||||
|
||||
@@ -223,8 +223,8 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
child: child,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -393,16 +393,17 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 16,
|
||||
width: 238,
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
constraints: BoxConstraints.expand(
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -468,8 +469,8 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
child: widget.channelWidget,
|
||||
channel: client,
|
||||
child: widget.channelWidget,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -505,12 +506,12 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: ChannelBottomSheet(
|
||||
onViewInfoTap: () {
|
||||
widget.onViewInfoTap(channel);
|
||||
},
|
||||
),
|
||||
channel: channel,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -553,7 +554,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
ChannelPreview(
|
||||
onLongPress: widget.onChannelLongPress,
|
||||
channel: channel,
|
||||
onImageTap: widget.onImageTap?.call(channel),
|
||||
onImageTap: () => widget.onImageTap?.call(channel),
|
||||
onTap: (channel) => onTap(channel, widget.channelWidget),
|
||||
),
|
||||
),
|
||||
@@ -591,13 +592,13 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: ChannelName(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
channel: channel,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -78,12 +78,12 @@ class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
child: StreamSvgIcon.left(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -32,16 +32,37 @@ class ChannelPreview extends StatelessWidget {
|
||||
/// The function called when the image is tapped
|
||||
final VoidCallback onImageTap;
|
||||
|
||||
/// Widget rendering the title
|
||||
final Widget title;
|
||||
|
||||
/// Widget rendering the subtitle
|
||||
final Widget subtitle;
|
||||
|
||||
/// Widget rendering the leading element, by default it shows the [ChannelImage]
|
||||
final Widget leading;
|
||||
|
||||
/// Widget rendering the trailing element, by default it shows the last message date
|
||||
final Widget trailing;
|
||||
|
||||
/// Widget rendering the sending indicator, by default it uses the [SendingIndicator] widget
|
||||
final Widget sendingIndicator;
|
||||
|
||||
ChannelPreview({
|
||||
@required this.channel,
|
||||
Key key,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onImageTap,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.sendingIndicator,
|
||||
this.trailing,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
|
||||
return StreamBuilder<bool>(
|
||||
stream: channel.isMutedStream,
|
||||
initialData: channel.isMuted,
|
||||
@@ -49,6 +70,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
return Opacity(
|
||||
opacity: snapshot.data ? 0.5 : 1,
|
||||
child: ListTile(
|
||||
visualDensity: VisualDensity.compact,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
@@ -62,17 +84,18 @@ class ChannelPreview extends StatelessWidget {
|
||||
onLongPress(channel);
|
||||
}
|
||||
},
|
||||
leading: ChannelImage(
|
||||
onTap: onImageTap,
|
||||
),
|
||||
leading: leading ??
|
||||
ChannelImage(
|
||||
onTap: onImageTap,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: ChannelName(
|
||||
textStyle:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.title,
|
||||
),
|
||||
child: title ??
|
||||
ChannelName(
|
||||
textStyle: channelPreviewTheme.title,
|
||||
),
|
||||
),
|
||||
StreamBuilder<List<Member>>(
|
||||
stream: channel.state.membersStream,
|
||||
@@ -93,37 +116,36 @@ class ChannelPreview extends StatelessWidget {
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(child: _buildSubtitle(context)),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final lastMessage = channel.state.messages.lastWhere(
|
||||
(m) => !m.isDeleted && m.shadowed != true,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (lastMessage?.user?.id ==
|
||||
StreamChat.of(context).user.id) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: SendingIndicator(
|
||||
message: lastMessage,
|
||||
size: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.indicatorIconSize,
|
||||
isMessageRead: channel.state.read
|
||||
?.where((element) =>
|
||||
element.user.id !=
|
||||
channel.client.state.user.id)
|
||||
?.where((element) => element.lastRead
|
||||
.isAfter(lastMessage.createdAt))
|
||||
?.isNotEmpty ==
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox();
|
||||
},
|
||||
),
|
||||
_buildDate(context),
|
||||
Flexible(child: subtitle ?? _buildSubtitle(context)),
|
||||
sendingIndicator ??
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final lastMessage = channel.state.messages.lastWhere(
|
||||
(m) => !m.isDeleted && m.shadowed != true,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (lastMessage?.user?.id ==
|
||||
StreamChat.of(context).user.id) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: SendingIndicator(
|
||||
message: lastMessage,
|
||||
size: channelPreviewTheme.indicatorIconSize,
|
||||
isMessageRead: channel.state.read
|
||||
?.where((element) =>
|
||||
element.user.id !=
|
||||
channel.client.state.user.id)
|
||||
?.where((element) => element.lastRead
|
||||
.isAfter(lastMessage.createdAt))
|
||||
?.isNotEmpty ==
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox();
|
||||
},
|
||||
),
|
||||
trailing ?? _buildDate(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -176,15 +198,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
),
|
||||
Text(
|
||||
' Channel is muted',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.color,
|
||||
),
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -192,10 +206,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
return TypingIndicator(
|
||||
channel: channel,
|
||||
alternativeWidget: _buildLastMessage(context),
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
color:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
|
||||
),
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:characters/characters.dart';
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
final _emojis = Emoji.all();
|
||||
@@ -44,3 +45,55 @@ extension PlatformFileX on PlatformFile {
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
extension InputDecorationX on InputDecoration {
|
||||
InputDecoration merge(InputDecoration other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
icon: other?.icon,
|
||||
labelText: other?.labelText,
|
||||
labelStyle: labelStyle?.merge(other.labelStyle) ?? other.labelStyle,
|
||||
helperText: other?.helperText,
|
||||
helperStyle: helperStyle?.merge(other.helperStyle) ?? other.helperStyle,
|
||||
helperMaxLines: other?.helperMaxLines,
|
||||
hintText: other?.hintText,
|
||||
hintStyle: hintStyle?.merge(other.hintStyle) ?? other.hintStyle,
|
||||
hintTextDirection: other?.hintTextDirection,
|
||||
hintMaxLines: other?.hintMaxLines,
|
||||
errorText: other?.errorText,
|
||||
errorStyle: errorStyle?.merge(other.errorStyle) ?? other.errorStyle,
|
||||
errorMaxLines: other?.errorMaxLines,
|
||||
floatingLabelBehavior: other?.floatingLabelBehavior,
|
||||
isCollapsed: other?.isCollapsed,
|
||||
isDense: other?.isDense,
|
||||
contentPadding: other?.contentPadding,
|
||||
prefixIcon: other?.prefixIcon,
|
||||
prefix: other?.prefix,
|
||||
prefixText: other?.prefixText,
|
||||
prefixIconConstraints: other?.prefixIconConstraints,
|
||||
prefixStyle: prefixStyle?.merge(other.prefixStyle) ?? other.prefixStyle,
|
||||
suffixIcon: other?.suffixIcon,
|
||||
suffix: other?.suffix,
|
||||
suffixText: other?.suffixText,
|
||||
suffixStyle: suffixStyle?.merge(other.suffixStyle) ?? other.suffixStyle,
|
||||
suffixIconConstraints: other?.suffixIconConstraints,
|
||||
counter: other?.counter,
|
||||
counterText: other?.counterText,
|
||||
counterStyle:
|
||||
counterStyle?.merge(other.counterStyle) ?? other.counterStyle,
|
||||
filled: other?.filled,
|
||||
fillColor: other?.fillColor,
|
||||
focusColor: other?.focusColor,
|
||||
hoverColor: other?.hoverColor,
|
||||
errorBorder: other?.errorBorder,
|
||||
focusedBorder: other?.focusedBorder,
|
||||
focusedErrorBorder: other?.focusedErrorBorder,
|
||||
disabledBorder: other?.disabledBorder,
|
||||
enabledBorder: other?.enabledBorder,
|
||||
border: other?.border,
|
||||
enabled: other?.enabled,
|
||||
semanticCounterText: other?.semanticCounterText,
|
||||
alignLabelWithHint: other?.alignLabelWithHint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ class VideoPackage {
|
||||
|
||||
ChewieController get chewieController => _chewieController;
|
||||
|
||||
bool get initialized => _videoPlayerController.value.initialized;
|
||||
bool get initialized => _videoPlayerController.value.isInitialized;
|
||||
|
||||
VideoPackage(
|
||||
Attachment attachment, {
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'extension.dart';
|
||||
|
||||
class ImageActionsModal extends StatelessWidget {
|
||||
final Message message;
|
||||
final String userName;
|
||||
final String sentAt;
|
||||
final List<Attachment> urls;
|
||||
final currentIndex;
|
||||
final VoidCallback onShowMessage;
|
||||
|
||||
ImageActionsModal(
|
||||
{this.message,
|
||||
this.userName,
|
||||
this.sentAt,
|
||||
this.urls,
|
||||
this.currentIndex,
|
||||
this.onShowMessage});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Navigator.maybePop(context),
|
||||
child: _buildPage(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPage(context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(height: kToolbarHeight),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Container(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildButton(
|
||||
context,
|
||||
'Reply',
|
||||
StreamSvgIcon.iconCurveLineLeftUp(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
() {
|
||||
Navigator.pop(context, ReturnActionType.reply);
|
||||
},
|
||||
),
|
||||
_buildButton(
|
||||
context,
|
||||
'Show in Chat',
|
||||
StreamSvgIcon.eye(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onShowMessage,
|
||||
),
|
||||
_buildButton(
|
||||
context,
|
||||
'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
|
||||
StreamSvgIcon.iconSave(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
() async {
|
||||
var url = urls[currentIndex].imageUrl ??
|
||||
urls[currentIndex].assetUrl ??
|
||||
urls[currentIndex].thumbUrl;
|
||||
|
||||
Navigator.pop(context);
|
||||
|
||||
if (urls[currentIndex].type == 'video') {
|
||||
await _saveVideo(url);
|
||||
} else {
|
||||
await _saveImage(url);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (StreamChat.of(context).user.id == message.user.id)
|
||||
_buildButton(
|
||||
context,
|
||||
'Delete',
|
||||
StreamSvgIcon.delete(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
() {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (message.attachments.length > 1 ||
|
||||
message.text.isNotEmpty) {
|
||||
final remainingAttachments = [...message.attachments]
|
||||
..removeAt(currentIndex);
|
||||
channel.updateMessage(message.copyWith(
|
||||
attachments: remainingAttachments,
|
||||
));
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
channel.deleteMessage(message).then((value) {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
});
|
||||
}
|
||||
},
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
]
|
||||
.map<Widget>((e) =>
|
||||
Align(alignment: Alignment.centerRight, child: e))
|
||||
.insertBetween(
|
||||
Container(
|
||||
height: 1,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButton(
|
||||
context,
|
||||
String title,
|
||||
StreamSvgIcon icon,
|
||||
VoidCallback onTap, {
|
||||
Color color,
|
||||
}) {
|
||||
return Material(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
icon,
|
||||
SizedBox(width: 16),
|
||||
Text(
|
||||
title,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveImage(String url) async {
|
||||
var response = await Dio()
|
||||
.get(url, options: Options(responseType: ResponseType.bytes));
|
||||
final result = await ImageGallerySaver.saveImage(
|
||||
Uint8List.fromList(response.data),
|
||||
quality: 60,
|
||||
name: '${DateTime.now().millisecondsSinceEpoch}');
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _saveVideo(String url) async {
|
||||
var appDocDir = await getTemporaryDirectory();
|
||||
var savePath =
|
||||
appDocDir.path + '/${DateTime.now().millisecondsSinceEpoch}.mp4';
|
||||
await Dio().download(url, savePath);
|
||||
final result = await ImageGallerySaver.saveFile(savePath);
|
||||
print(result);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,10 @@ import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
@@ -90,27 +91,40 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.iconShare(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () async {
|
||||
final attachment =
|
||||
widget.mediaAttachments[widget.currentPage];
|
||||
var url = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
var type = attachment.type == 'image'
|
||||
? 'jpg'
|
||||
: url?.split('?')?.first?.split('.')?.last ?? 'jpg';
|
||||
var request = await HttpClient().getUrl(Uri.parse(url));
|
||||
var response = await request.close();
|
||||
var bytes =
|
||||
await consolidateHttpClientResponseBytes(response);
|
||||
await Share.file('File', 'image.$type', bytes, 'image/$type');
|
||||
},
|
||||
),
|
||||
kIsWeb
|
||||
? SizedBox()
|
||||
: IconButton(
|
||||
icon: StreamSvgIcon.iconShare(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () async {
|
||||
final attachment =
|
||||
widget.mediaAttachments[widget.currentPage];
|
||||
final url = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
final type = attachment.type == 'image'
|
||||
? 'jpg'
|
||||
: url?.split('?')?.first?.split('.')?.last ?? 'jpg';
|
||||
final request =
|
||||
await HttpClient().getUrl(Uri.parse(url));
|
||||
final response = await request.close();
|
||||
final bytes =
|
||||
await consolidateHttpClientResponseBytes(response);
|
||||
final tmpPath = await getTemporaryDirectory();
|
||||
final filePath =
|
||||
'${tmpPath.path}/${attachment.id}.$type';
|
||||
final file = File(filePath);
|
||||
await file.writeAsBytes(bytes);
|
||||
await Share.shareFiles(
|
||||
[filePath],
|
||||
mimeTypes: [
|
||||
'image/$type',
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
InkWell(
|
||||
onTap: widget.onTitleTap,
|
||||
child: Container(
|
||||
@@ -223,13 +237,13 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
media = InkWell(
|
||||
onTap: () => widget.mediaSelectedCallBack(index),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
aspectRatio: 1.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import 'image_actions_modal.dart';
|
||||
import 'attachment_actions_modal.dart';
|
||||
|
||||
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// True if this header shows the leading back button
|
||||
@@ -110,11 +110,8 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: ImageActionsModal(
|
||||
userName: userName,
|
||||
sentAt: sentAt,
|
||||
child: AttachmentActionsModal(
|
||||
message: message,
|
||||
urls: urls,
|
||||
currentIndex: currentIndex,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
|
||||
@@ -56,6 +56,11 @@ class _MediaListViewState extends State<MediaListView> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 1.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (widget.onSelect != null) {
|
||||
widget.onSelect(media);
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
@@ -125,11 +130,6 @@ class _MediaListViewState extends State<MediaListView> {
|
||||
]
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.onSelect != null) {
|
||||
widget.onSelect(media);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Class describing a message action
|
||||
class MessageAction {
|
||||
/// leading widget
|
||||
final Widget leading;
|
||||
|
||||
/// title widget
|
||||
final Widget title;
|
||||
|
||||
/// callback called on tap
|
||||
final OnMessageTap onTap;
|
||||
|
||||
/// returns a new instance of a [MessageAction]
|
||||
MessageAction({
|
||||
this.leading,
|
||||
this.title,
|
||||
this.onTap,
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'dart:ui';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:stream_chat_flutter/src/message_action.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_picker.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
@@ -34,6 +35,9 @@ class MessageActionsModal extends StatefulWidget {
|
||||
final ShapeBorder attachmentShape;
|
||||
final DisplayWidget showUserAvatar;
|
||||
|
||||
/// List of custom actions
|
||||
final List<MessageAction> customActions;
|
||||
|
||||
const MessageActionsModal({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@@ -53,6 +57,7 @@ class MessageActionsModal extends StatefulWidget {
|
||||
this.messageShape,
|
||||
this.attachmentShape,
|
||||
this.reverse = false,
|
||||
this.customActions = const [],
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -223,6 +228,12 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
_buildFlagButton(context),
|
||||
if (widget.showDeleteMessage)
|
||||
_buildDeleteButton(context),
|
||||
...widget.customActions.map((action) {
|
||||
return _buildCustomAction(
|
||||
context,
|
||||
action,
|
||||
);
|
||||
})
|
||||
].insertBetween(
|
||||
Container(
|
||||
height: 1,
|
||||
@@ -248,27 +259,69 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
);
|
||||
}
|
||||
|
||||
InkWell _buildCustomAction(
|
||||
BuildContext context,
|
||||
MessageAction messageAction,
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
messageAction.onTap?.call(widget.message);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 11.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
messageAction.leading ?? Offstage(),
|
||||
const SizedBox(width: 16),
|
||||
messageAction.title ?? Offstage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFlagDialog() async {
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
var answer = await showConfirmationDialog(context,
|
||||
title: 'Flag Message',
|
||||
icon: StreamSvgIcon.flag(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
question:
|
||||
'Do you want to send a copy of this message to a\nmoderator for further investigation?',
|
||||
okText: 'FLAG',
|
||||
cancelText: 'CANCEL');
|
||||
final answer = await showConfirmationDialog(
|
||||
context,
|
||||
title: 'Flag Message',
|
||||
icon: StreamSvgIcon.flag(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
question:
|
||||
'Do you want to send a copy of this message to a\nmoderator for further investigation?',
|
||||
okText: 'FLAG',
|
||||
cancelText: 'CANCEL',
|
||||
);
|
||||
|
||||
if (answer) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
if (answer == true) {
|
||||
try {
|
||||
await client.flagMessage(widget.message.id);
|
||||
_showDismissAlert();
|
||||
await showInfoDialog(
|
||||
context,
|
||||
icon: StreamSvgIcon.flag(
|
||||
color: theme.colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
details: 'The message has been reported to a moderator.',
|
||||
title: 'Message flagged',
|
||||
okText: 'OK',
|
||||
);
|
||||
} catch (err) {
|
||||
if (json.decode(err?.body ?? {})['code'] == 4) {
|
||||
_showDismissAlert();
|
||||
await showInfoDialog(
|
||||
context,
|
||||
icon: StreamSvgIcon.flag(
|
||||
color: theme.colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
details: 'The message has been reported to a moderator.',
|
||||
title: 'Message flagged',
|
||||
okText: 'OK',
|
||||
);
|
||||
} else {
|
||||
_showErrorAlert();
|
||||
}
|
||||
@@ -306,133 +359,16 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showDismissAlert() {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
context: context,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
)),
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
StreamSvgIcon.flag(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
Text(
|
||||
'Message flagged',
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
SizedBox(
|
||||
height: 7.0,
|
||||
),
|
||||
Text('The message has been reported to a moderator.'),
|
||||
SizedBox(
|
||||
height: 36.0,
|
||||
),
|
||||
Container(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
|
||||
height: 1.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
'OK',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showErrorAlert() {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
context: context,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
)),
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
StreamSvgIcon.error(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
Text(
|
||||
'Something went wrong',
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
SizedBox(
|
||||
height: 7.0,
|
||||
),
|
||||
Text('The operation couldn\'t be completed.'),
|
||||
SizedBox(
|
||||
height: 36.0,
|
||||
),
|
||||
Container(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
|
||||
height: 1.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
'OK',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
showInfoDialog(
|
||||
context,
|
||||
icon: StreamSvgIcon.error(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
size: 24.0,
|
||||
),
|
||||
details: 'The operation couldn\'t be completed.',
|
||||
title: 'Something went wrong',
|
||||
okText: 'OK',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -637,21 +573,16 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: widget.editMessageInputBuilder != null
|
||||
? widget.editMessageInputBuilder(context, widget.message)
|
||||
: MessageInput(
|
||||
editMessage: widget.message,
|
||||
preMessageSending: (m) {
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context);
|
||||
return m;
|
||||
},
|
||||
),
|
||||
),
|
||||
widget.editMessageInputBuilder != null
|
||||
? widget.editMessageInputBuilder(context, widget.message)
|
||||
: MessageInput(
|
||||
editMessage: widget.message,
|
||||
preMessageSending: (m) {
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context);
|
||||
return m;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/video_service.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||||
@@ -42,6 +43,15 @@ enum DefaultAttachmentTypes {
|
||||
file,
|
||||
}
|
||||
|
||||
/// Available locations for the sendMessage button relative to the textField
|
||||
enum SendButtonLocation {
|
||||
/// inside the textField
|
||||
inside,
|
||||
|
||||
/// outside the textField
|
||||
outside,
|
||||
}
|
||||
|
||||
const _kMinMediaPickerSize = 360.0;
|
||||
|
||||
const _kMaxAttachmentSize = 20971520; // 20MB in Bytes
|
||||
@@ -107,6 +117,11 @@ class MessageInput extends StatefulWidget {
|
||||
this.focusNode,
|
||||
this.quotedMessage,
|
||||
this.onQuotedMessageCleared,
|
||||
this.sendButtonLocation = SendButtonLocation.outside,
|
||||
this.autofocus = false,
|
||||
this.hideSendAsDm = false,
|
||||
this.idleSendButton,
|
||||
this.activeSendButton,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Message to edit
|
||||
@@ -134,6 +149,9 @@ class MessageInput extends StatefulWidget {
|
||||
/// If true the attachments button will not be displayed
|
||||
final bool disableAttachments;
|
||||
|
||||
/// Hide send as dm checkbox
|
||||
final bool hideSendAsDm;
|
||||
|
||||
/// The text controller of the TextField
|
||||
final TextEditingController textEditingController;
|
||||
|
||||
@@ -155,6 +173,18 @@ class MessageInput extends StatefulWidget {
|
||||
///
|
||||
final VoidCallback onQuotedMessageCleared;
|
||||
|
||||
/// The location of the send button
|
||||
final SendButtonLocation sendButtonLocation;
|
||||
|
||||
/// Autofocus property passed to the TextField
|
||||
final bool autofocus;
|
||||
|
||||
/// Send button widget in an idle state
|
||||
final Widget idleSendButton;
|
||||
|
||||
/// Send button widget in an active state
|
||||
final Widget activeSendButton;
|
||||
|
||||
@override
|
||||
MessageInputState createState() => MessageInputState();
|
||||
|
||||
@@ -281,7 +311,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _buildTextField(context),
|
||||
),
|
||||
if (widget.parentMessage != null)
|
||||
if (widget.parentMessage != null && !widget.hideSendAsDm)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: _buildDmCheckbox(),
|
||||
@@ -304,15 +334,16 @@ class MessageInputState extends State<MessageInput> {
|
||||
Flex _buildTextField(BuildContext context) {
|
||||
return Flex(
|
||||
direction: Axis.horizontal,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
if (!_commandEnabled) _buildExpandActionsButton(),
|
||||
if (widget.actionsLocation == ActionsLocation.left)
|
||||
...widget.actions ?? [],
|
||||
_buildTextInput(context),
|
||||
_animateSendButton(context),
|
||||
if (widget.actionsLocation == ActionsLocation.right)
|
||||
...widget.actions ?? [],
|
||||
if (widget.sendButtonLocation == SendButtonLocation.outside)
|
||||
_animateSendButton(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -384,14 +415,20 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
Widget _animateSendButton(BuildContext context) {
|
||||
final sendButton = widget.activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: sendMessage,
|
||||
child: widget.activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AnimatedCrossFade(
|
||||
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: _buildSendButton(context),
|
||||
secondChild: _buildIdleSendButton(context),
|
||||
firstChild: sendButton,
|
||||
secondChild: widget.idleSendButton ?? _buildIdleSendButton(context),
|
||||
duration:
|
||||
StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration,
|
||||
alignment: Alignment.center,
|
||||
@@ -447,101 +484,139 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
border: Border.all(color: theme.colorTheme.greyGainsboro),
|
||||
borderRadius: theme.messageInputTheme.borderRadius,
|
||||
gradient: _focusNode.hasFocus
|
||||
? theme.messageInputTheme.activeBorderGradient
|
||||
: theme.messageInputTheme.idleBorderGradient,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildReplyToMessage(),
|
||||
_buildAttachments(),
|
||||
LimitedBox(
|
||||
maxHeight: widget.maxHeight,
|
||||
child: TextField(
|
||||
key: Key('messageInputText'),
|
||||
enabled: _inputEnabled,
|
||||
minLines: null,
|
||||
maxLines: null,
|
||||
onSubmitted: (_) => sendMessage(),
|
||||
keyboardType: widget.keyboardType,
|
||||
controller: textEditingController,
|
||||
focusNode: _focusNode,
|
||||
style: theme.textTheme.body,
|
||||
autofocus: false,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
hintText: _getHint(),
|
||||
hintStyle: theme.textTheme.body.copyWith(
|
||||
color: theme.colorTheme.grey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(1.5),
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: theme.messageInputTheme.borderRadius,
|
||||
color: theme.messageInputTheme.inputBackground,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildReplyToMessage(),
|
||||
_buildAttachments(),
|
||||
LimitedBox(
|
||||
maxHeight: widget.maxHeight,
|
||||
child: TextField(
|
||||
key: Key('messageInputText'),
|
||||
enabled: _inputEnabled,
|
||||
minLines: null,
|
||||
maxLines: null,
|
||||
onSubmitted: (_) => sendMessage(),
|
||||
keyboardType: widget.keyboardType,
|
||||
controller: textEditingController,
|
||||
focusNode: _focusNode,
|
||||
style: theme.messageInputTheme.inputTextStyle,
|
||||
autofocus: widget.autofocus,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: _getInputDecoration(),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent)),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent)),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.transparent)),
|
||||
contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11),
|
||||
prefixIconConstraints: BoxConstraints.tight(Size(78, 24)),
|
||||
suffixIconConstraints: BoxConstraints.tight(Size(40, 40)),
|
||||
prefixIcon: _commandEnabled
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorTheme.accentBlue,
|
||||
),
|
||||
margin: const EdgeInsets.only(right: 4, left: 8),
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color: Colors.white,
|
||||
size: 16.0,
|
||||
),
|
||||
Text(
|
||||
_chosenCommand?.name?.toUpperCase() ?? '',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnoteBold
|
||||
.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
suffixIcon: _commandEnabled
|
||||
? IconButton(
|
||||
icon: StreamSvgIcon.closeSmall(),
|
||||
splashRadius: 24,
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _commandEnabled = false);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration _getInputDecoration() {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
final passedDecoration = theme.messageInputTheme.inputDecoration;
|
||||
return InputDecoration(
|
||||
isDense: true,
|
||||
hintText: _getHint(),
|
||||
hintStyle: theme.messageInputTheme.inputTextStyle.copyWith(
|
||||
color: theme.colorTheme.grey,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11),
|
||||
prefixIconConstraints: BoxConstraints.tight(Size(78, 24)),
|
||||
suffixIconConstraints: BoxConstraints.tight(Size(40, 40)),
|
||||
prefixIcon: _commandEnabled
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorTheme.accentBlue,
|
||||
),
|
||||
margin: const EdgeInsets.only(right: 4, left: 8),
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color: Colors.white,
|
||||
size: 16.0,
|
||||
),
|
||||
Text(
|
||||
_chosenCommand?.name?.toUpperCase() ?? '',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnoteBold
|
||||
.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
suffixIcon: Row(
|
||||
children: [
|
||||
if (_commandEnabled)
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.closeSmall(),
|
||||
splashRadius: 24,
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _commandEnabled = false);
|
||||
},
|
||||
),
|
||||
if (widget.sendButtonLocation == SendButtonLocation.inside)
|
||||
_animateSendButton(context),
|
||||
],
|
||||
),
|
||||
).merge(passedDecoration);
|
||||
}
|
||||
|
||||
Timer _debounce;
|
||||
|
||||
void _onChanged(BuildContext context, String s) {
|
||||
@@ -621,7 +696,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
.last
|
||||
.contains('@')) {
|
||||
_mentionsOverlay = _buildMentionsOverlayEntry();
|
||||
Overlay.of(context).insert(_mentionsOverlay);
|
||||
if (_mentionsOverlay != null) {
|
||||
Overlay.of(context).insert(_mentionsOverlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,7 +723,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
_commandsOverlay = null;
|
||||
} else {
|
||||
_commandsOverlay = _buildCommandsOverlayEntry();
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
if (_commandsOverlay != null) {
|
||||
Overlay.of(context).insert(_commandsOverlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -661,6 +740,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
?.toList() ??
|
||||
[];
|
||||
|
||||
if (commands.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RenderBox renderBox = context.findRenderObject();
|
||||
final size = renderBox.size;
|
||||
|
||||
@@ -833,6 +916,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: Material(
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@@ -930,7 +1014,19 @@ class MessageInputState extends State<MessageInput> {
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: _buildPickerSection(),
|
||||
child: _PickerWidget(
|
||||
filePickerIndex: _filePickerIndex,
|
||||
containsFile: _attachmentContainsFile,
|
||||
selectedMedias: _attachments.keys.toList(),
|
||||
onAddMoreFilesClick: pickFile,
|
||||
onMediaSelected: (media) {
|
||||
if (_attachments.containsKey(media.id)) {
|
||||
setState(() => _attachments.remove(media.id));
|
||||
} else {
|
||||
_addAttachment(media);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -939,109 +1035,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPickerSection() {
|
||||
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||
return it.type == 'file';
|
||||
});
|
||||
|
||||
switch (_filePickerIndex) {
|
||||
case 0:
|
||||
return FutureBuilder<bool>(
|
||||
future: PhotoManager.requestPermission(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.data) {
|
||||
if (_attachmentContainsFile) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
pickFile(DefaultAttachmentTypes.file);
|
||||
},
|
||||
child: Container(
|
||||
constraints: BoxConstraints.expand(),
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Text(
|
||||
'Add more files',
|
||||
style: TextStyle(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
return MediaListView(
|
||||
selectedIds: _attachments.keys.toList(),
|
||||
onSelect: (media) async {
|
||||
if (!_attachments.containsKey(media.id)) {
|
||||
_addAttachment(media);
|
||||
} else {
|
||||
setState(() => _attachments.remove(media.id));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
PhotoManager.openSetting();
|
||||
},
|
||||
child: Container(
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'svgs/icon_picture_empty_state.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
height: 140,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyGainsboro,
|
||||
),
|
||||
Text(
|
||||
'Please enable access to your photos \nand videos so you can share them with friends.',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 6.0),
|
||||
Center(
|
||||
child: Text(
|
||||
'Allow access to your gallery',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
void _addAttachment(AssetEntity medium) async {
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
Duration(seconds: 5),
|
||||
@@ -1090,81 +1083,81 @@ class MessageInputState extends State<MessageInput> {
|
||||
switch (iconType) {
|
||||
case 'giphy':
|
||||
return CircleAvatar(
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.giphyIcon(
|
||||
size: 24.0,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'ban':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.iconUserDelete(
|
||||
size: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'flag':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.flag(
|
||||
size: 14.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'imgur':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: ClipOval(
|
||||
child: StreamSvgIcon.imgur(
|
||||
size: 24.0,
|
||||
),
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'mute':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.mute(
|
||||
size: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'unban':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.userAdd(
|
||||
size: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
case 'unmute':
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.volumeUp(
|
||||
size: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.lightning(
|
||||
size: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
radius: 12,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -1191,6 +1184,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
})?.toList() ??
|
||||
[];
|
||||
|
||||
if (members.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RenderBox renderBox = context.findRenderObject();
|
||||
final size = renderBox.size;
|
||||
|
||||
@@ -1646,11 +1643,15 @@ class MessageInputState extends State<MessageInput> {
|
||||
return getFileTypeImage(attachment.extraData['other']);
|
||||
},
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
return Shimmer.fromColors(
|
||||
baseColor:
|
||||
StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor:
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Image.asset(
|
||||
'images/placeholder.png',
|
||||
fit: BoxFit.cover,
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -1676,8 +1677,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
default:
|
||||
return Container(
|
||||
child: Icon(Icons.insert_drive_file),
|
||||
color: Colors.black26,
|
||||
child: Icon(Icons.insert_drive_file),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1903,14 +1904,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
if (file == null) return;
|
||||
|
||||
final mimeType = file.path.split('/').last.mimeType;
|
||||
final mimeType = file.name?.mimeType;
|
||||
|
||||
final extraDataMap = <String, dynamic>{};
|
||||
|
||||
if (camera) {
|
||||
if (mimeType.type == 'video' || mimeType.type == 'image') {
|
||||
attachmentType = mimeType.type;
|
||||
}
|
||||
if (mimeType.type == 'video' || mimeType.type == 'image') {
|
||||
attachmentType = mimeType.type;
|
||||
} else {
|
||||
attachmentType = 'file';
|
||||
}
|
||||
@@ -2013,9 +2012,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldUnfocus = _commandEnabled;
|
||||
|
||||
if (_commandEnabled) {
|
||||
text = '/${_chosenCommand.name} ' + text;
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
final attachments = [..._attachments.values];
|
||||
@@ -2082,7 +2082,14 @@ class MessageInputState extends State<MessageInput> {
|
||||
sendingFuture = channel.updateMessage(message);
|
||||
}
|
||||
|
||||
if (!shouldUnfocus) {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
}
|
||||
|
||||
return sendingFuture.then((resp) {
|
||||
if (resp.message?.type == 'error') {
|
||||
_parseExistingMessage(message);
|
||||
}
|
||||
if (widget.onMessageSent != null) {
|
||||
widget.onMessageSent(resp.message);
|
||||
}
|
||||
@@ -2140,6 +2147,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
'OK',
|
||||
style: StreamChatTheme.of(context)
|
||||
@@ -2150,9 +2160,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
.colorTheme
|
||||
.accentBlue),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -2240,3 +2247,114 @@ class Tuple2<T1, T2> {
|
||||
@override
|
||||
int get hashCode => item1.hashCode ^ item2.hashCode;
|
||||
}
|
||||
|
||||
class _PickerWidget extends StatefulWidget {
|
||||
final int filePickerIndex;
|
||||
final bool containsFile;
|
||||
final List<String> selectedMedias;
|
||||
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
|
||||
final void Function(AssetEntity) onMediaSelected;
|
||||
|
||||
const _PickerWidget({
|
||||
Key key,
|
||||
@required this.filePickerIndex,
|
||||
@required this.containsFile,
|
||||
@required this.selectedMedias,
|
||||
@required this.onAddMoreFilesClick,
|
||||
@required this.onMediaSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
__PickerWidgetState createState() => __PickerWidgetState();
|
||||
}
|
||||
|
||||
class __PickerWidgetState extends State<_PickerWidget> {
|
||||
Future<bool> requestPermission;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
requestPermission = PhotoManager.requestPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.filePickerIndex != 0) {
|
||||
return Offstage();
|
||||
}
|
||||
return FutureBuilder<bool>(
|
||||
future: requestPermission,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.data) {
|
||||
if (widget.containsFile) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onAddMoreFilesClick(DefaultAttachmentTypes.file);
|
||||
},
|
||||
child: Container(
|
||||
constraints: BoxConstraints.expand(),
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Add more files',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return MediaListView(
|
||||
selectedIds: widget.selectedMedias,
|
||||
onSelect: widget.onMediaSelected,
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
PhotoManager.openSetting();
|
||||
},
|
||||
child: Container(
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'svgs/icon_picture_empty_state.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
height: 140,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
Text(
|
||||
'Please enable access to your photos \nand videos so you can share them with friends.',
|
||||
style: StreamChatTheme.of(context).textTheme.body.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 6.0),
|
||||
Center(
|
||||
child: Text(
|
||||
'Allow access to your gallery',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,15 @@ typedef ParentMessageBuilder = Widget Function(
|
||||
BuildContext,
|
||||
Message,
|
||||
);
|
||||
typedef SystemMessageBuilder = Widget Function(
|
||||
BuildContext,
|
||||
Message,
|
||||
);
|
||||
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
|
||||
typedef ThreadTapCallback = void Function(Message, Widget);
|
||||
|
||||
typedef OnMessageSwiped = void Function(Message);
|
||||
typedef OnMessageTap = void Function(Message);
|
||||
typedef ReplyTapCallback = void Function(Message);
|
||||
|
||||
class MessageDetails {
|
||||
@@ -128,14 +133,22 @@ class MessageListView extends StatefulWidget {
|
||||
this.showConnectionStateTile = false,
|
||||
this.loadingBuilder,
|
||||
this.emptyBuilder,
|
||||
this.systemMessageBuilder,
|
||||
this.messageListBuilder,
|
||||
this.errorWidgetBuilder,
|
||||
this.messageFilter,
|
||||
this.customAttachmentBuilders,
|
||||
this.onMessageTap,
|
||||
this.onSystemMessageTap,
|
||||
this.onAttachmentTap,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Function used to build a custom message widget
|
||||
final MessageBuilder messageBuilder;
|
||||
|
||||
/// Function used to build a custom system message widget
|
||||
final SystemMessageBuilder systemMessageBuilder;
|
||||
|
||||
/// Function used to build a custom parent message widget
|
||||
final ParentMessageBuilder parentMessageBuilder;
|
||||
|
||||
@@ -204,10 +217,22 @@ class MessageListView extends StatefulWidget {
|
||||
/// of a connection failure.
|
||||
final ErrorBuilder errorWidgetBuilder;
|
||||
|
||||
/// Predicate used to filter messages
|
||||
final bool Function(Message) messageFilter;
|
||||
|
||||
/// Attachment builders for the default message widget
|
||||
/// Please change this in the [MessageWidget] if you are using a custom implementation
|
||||
final Map<String, AttachmentBuilder> customAttachmentBuilders;
|
||||
|
||||
/// Called when any message is tapped except a system message (use [onSystemMessageTap] instead)
|
||||
final OnMessageTap onMessageTap;
|
||||
|
||||
/// Called when system message is tapped
|
||||
final OnMessageTap onSystemMessageTap;
|
||||
|
||||
// Customize onTap on attachment
|
||||
final void Function(Message message, Attachment attachment) onAttachmentTap;
|
||||
|
||||
@override
|
||||
_MessageListViewState createState() => _MessageListViewState();
|
||||
}
|
||||
@@ -265,6 +290,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MessageListCore(
|
||||
messageFilter: widget.messageFilter,
|
||||
loadingBuilder: widget.loadingBuilder ??
|
||||
(context) {
|
||||
return Center(
|
||||
@@ -356,6 +382,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
childAnchor: Alignment.topCenter,
|
||||
message: statusString,
|
||||
child: LazyLoadScrollView(
|
||||
onPageScrollStart: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
onStartOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
if (!_upToDate) {
|
||||
@@ -408,7 +437,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
.subtitle,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -595,9 +624,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: StreamSvgIcon.down(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () {
|
||||
if (unreadCount > 0) {
|
||||
streamChannel.channel.markRead();
|
||||
@@ -615,6 +641,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
);
|
||||
}
|
||||
},
|
||||
child: StreamSvgIcon.down(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
if (showUnreadCount)
|
||||
Positioned(
|
||||
@@ -804,6 +833,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
},
|
||||
customAttachmentBuilders: widget.customAttachmentBuilders,
|
||||
onMessageTap: (message) {
|
||||
if (widget.onMessageTap != null) {
|
||||
widget.onMessageTap(message);
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -812,11 +847,19 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
List<Message> messages,
|
||||
int index,
|
||||
) {
|
||||
if (message.type == 'system' && message.text?.isNotEmpty == true) {
|
||||
return SystemMessage(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
);
|
||||
if ((message.type == 'system' || message.type == 'error') &&
|
||||
message.text?.isNotEmpty == true) {
|
||||
return widget.systemMessageBuilder?.call(context, message) ??
|
||||
SystemMessage(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
onMessageTap: (message) {
|
||||
if (widget.onSystemMessageTap != null) {
|
||||
widget.onSystemMessageTap(message);
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final userId = StreamChat.of(context).user.id;
|
||||
@@ -965,6 +1008,13 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
},
|
||||
customAttachmentBuilders: widget.customAttachmentBuilders,
|
||||
onMessageTap: (message) {
|
||||
if (widget.onMessageTap != null) {
|
||||
widget.onMessageTap(message);
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
onAttachmentTap: widget.onAttachmentTap,
|
||||
);
|
||||
|
||||
if (!message.isDeleted &&
|
||||
@@ -995,10 +1045,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
end: colorTheme.white.withOpacity(0),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: child,
|
||||
),
|
||||
onEnd: () => initialMessageHighlightComplete = true,
|
||||
builder: (_, color, child) {
|
||||
return Container(
|
||||
@@ -1006,6 +1052,10 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
return child;
|
||||
|
||||
@@ -237,6 +237,9 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 6,
|
||||
left: isCurrentUser ? -3 : null,
|
||||
right: isCurrentUser ? -3 : null,
|
||||
child: Align(
|
||||
alignment:
|
||||
reverse ? Alignment.centerRight : Alignment.centerLeft,
|
||||
@@ -250,9 +253,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
highlightOwnReactions: false,
|
||||
),
|
||||
),
|
||||
bottom: 6,
|
||||
left: isCurrentUser ? -3 : null,
|
||||
right: isCurrentUser ? -3 : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -157,53 +157,14 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
errorBuilder: widget.errorBuilder ??
|
||||
(BuildContext context, dynamic error) {
|
||||
if (error is Error) {
|
||||
print((error).stackTrace);
|
||||
}
|
||||
|
||||
var message = error.toString();
|
||||
if (error is DioError) {
|
||||
if (error.type == DioErrorType.RESPONSE) {
|
||||
message = error.message;
|
||||
} else {
|
||||
message = 'Check your connection and retry';
|
||||
}
|
||||
print(error.stackTrace);
|
||||
}
|
||||
return InfoTile(
|
||||
showMessage: widget.showErrorTile,
|
||||
tileAnchor: Alignment.topCenter,
|
||||
childAnchor: Alignment.topCenter,
|
||||
message: 'An error occurred.',
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 2.0),
|
||||
child: Icon(Icons.error_outline),
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'Error loading messages'),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Text(message),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_messageSearchListController.loadData();
|
||||
},
|
||||
child: Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Container(),
|
||||
);
|
||||
},
|
||||
loadingBuilder: widget.loadingBuilder ??
|
||||
|
||||
@@ -21,7 +21,8 @@ class MessageText extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = _replaceMentions(message.text);
|
||||
final text = _replaceMentions(message.text).replaceAll('\n', '\\\n');
|
||||
|
||||
return MarkdownBody(
|
||||
data: text,
|
||||
onTapLink: (
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/src/message_action.dart';
|
||||
import 'package:stream_chat_flutter/src/message_actions_modal.dart';
|
||||
import 'package:stream_chat_flutter/src/message_reactions_modal.dart';
|
||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||
@@ -143,6 +144,15 @@ class MessageWidget extends StatefulWidget {
|
||||
/// Function called when quotedMessage is tapped
|
||||
final OnQuotedMessageTap onQuotedMessageTap;
|
||||
|
||||
/// Function called when message is tapped
|
||||
final void Function(Message) onMessageTap;
|
||||
|
||||
/// List of custom actions shown on message long tap
|
||||
final List<MessageAction> customActions;
|
||||
|
||||
// Customize onTap on attachment
|
||||
final void Function(Message message, Attachment attachment) onAttachmentTap;
|
||||
|
||||
///
|
||||
MessageWidget({
|
||||
Key key,
|
||||
@@ -157,6 +167,7 @@ class MessageWidget extends StatefulWidget {
|
||||
this.borderRadiusGeometry,
|
||||
this.attachmentBorderRadiusGeometry,
|
||||
this.onMentionTap,
|
||||
this.onMessageTap,
|
||||
this.showReactionPickerIndicator = false,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.showSendingIndicator = true,
|
||||
@@ -191,6 +202,8 @@ class MessageWidget extends StatefulWidget {
|
||||
this.attachmentPadding = EdgeInsets.zero,
|
||||
this.allRead = false,
|
||||
this.onQuotedMessageTap,
|
||||
this.customActions = const [],
|
||||
this.onAttachmentTap,
|
||||
}) : attachmentBuilders = {
|
||||
'image': (context, message, attachment) {
|
||||
return ImageAttachment(
|
||||
@@ -203,6 +216,11 @@ class MessageWidget extends StatefulWidget {
|
||||
),
|
||||
onShowMessage: onShowMessage,
|
||||
onReturnAction: onReturnAction,
|
||||
onAttachmentTap: onAttachmentTap != null
|
||||
? () {
|
||||
onAttachmentTap?.call(message, attachment);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
},
|
||||
'video': (context, message, attachment) {
|
||||
@@ -216,6 +234,11 @@ class MessageWidget extends StatefulWidget {
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
onReturnAction: onReturnAction,
|
||||
onAttachmentTap: onAttachmentTap != null
|
||||
? () {
|
||||
onAttachmentTap?.call(message, attachment);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
},
|
||||
'giphy': (context, message, attachment) {
|
||||
@@ -311,6 +334,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
type: MaterialType.transparency,
|
||||
child: Portal(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
widget.onMessageTap(widget.message);
|
||||
},
|
||||
onLongPress: widget.message.isDeleted && !isFailedState
|
||||
? null
|
||||
: () => onLongPress(context),
|
||||
@@ -351,9 +377,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
portal: Container(
|
||||
transform:
|
||||
Matrix4.translationValues(-12, 0, 0),
|
||||
child: _buildReactionIndicator(context),
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: 22 * 6.0),
|
||||
child: _buildReactionIndicator(context),
|
||||
),
|
||||
portalAnchor: Alignment(-1.0, -1.0),
|
||||
childAnchor: Alignment(1, -1.0),
|
||||
@@ -745,6 +771,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
!isFailedState &&
|
||||
widget.onThreadTap != null,
|
||||
showFlagButton: widget.showFlagButton,
|
||||
customActions: widget.customActions,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -951,6 +978,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
user: widget.message.user,
|
||||
onTap: widget.onUserAvatarTap,
|
||||
constraints: widget.messageTheme.avatarTheme.constraints,
|
||||
borderRadius: widget.messageTheme.avatarTheme.borderRadius,
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -55,7 +55,7 @@ class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
|
||||
return Container(
|
||||
height: widget.size.height,
|
||||
width: widget.size.width,
|
||||
child: _controller.value.initialized
|
||||
child: _controller.value.isInitialized
|
||||
? VideoPlayer(_controller)
|
||||
: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user