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:
Salvatore Giordano
2021-03-09 18:58:23 +01:00
committed by GitHub
co-authored by Sahil Kumar Deven Joshi Neevash Ramdial Nelson Nunes
parent 3332a48a0e
commit 4488fa6c3a
121 changed files with 3768 additions and 2376 deletions
+30
View File
@@ -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 }}
+1 -1
View File
@@ -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
+22
View File
@@ -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
View File
@@ -45,7 +45,7 @@ build/
.project
.classpath
.settings
/.fvm
**/.fvm
.melos_tool/
/packages/flutter_widgets/example/ios/Flutter/.last_build_id
+3
View File
@@ -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`.
+7
View File
@@ -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
+132 -48
View File
@@ -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
+235 -190
View File
@@ -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());
+37 -35
View File
@@ -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() {
+39 -38
View File
@@ -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,
+172 -158
View File
@@ -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;
}
+9 -10
View File
@@ -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);
}
+44 -52
View File
@@ -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
+26 -26
View File
@@ -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(
+10 -11
View File
@@ -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,
+48 -53
View File
@@ -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;
+4 -2
View File
@@ -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 -1
View File
@@ -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';
+12 -13
View File
@@ -1,7 +1,7 @@
name: stream_chat
homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications.
version: 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",
+14
View File
@@ -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
@@ -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"))
}
}
@@ -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);
}
}
@@ -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"
}
}
@@ -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;
}
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