[Async Attachment Upload] Initial implementation
Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
@@ -3,6 +3,7 @@ include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/**/*.g.dart
|
||||
- lib/**/*.freezed.dart
|
||||
- example/*
|
||||
- test/*
|
||||
|
||||
|
||||
+35
-11
@@ -1,56 +1,80 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'extension.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:stream_chat/src/models/attachment_file.dart';
|
||||
import '../client.dart';
|
||||
import '../extensions/string_extension.dart';
|
||||
|
||||
///
|
||||
abstract class AttachmentUploader {
|
||||
///
|
||||
Future<String> uploadImage(
|
||||
PlatformFile file, {
|
||||
AttachmentFile file,
|
||||
String channelId,
|
||||
String channelType, {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
});
|
||||
|
||||
///
|
||||
Future<String> uploadFile(
|
||||
PlatformFile file, {
|
||||
AttachmentFile file,
|
||||
String channelId,
|
||||
String channelType, {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
});
|
||||
}
|
||||
|
||||
///
|
||||
class StreamAttachmentUploader implements AttachmentUploader {
|
||||
final Channel _channel;
|
||||
final StreamChatClient _client;
|
||||
|
||||
const StreamAttachmentUploader(this._channel);
|
||||
///
|
||||
const StreamAttachmentUploader(this._client);
|
||||
|
||||
@override
|
||||
Future<String> uploadImage(
|
||||
PlatformFile file, {
|
||||
AttachmentFile file,
|
||||
String channelId,
|
||||
String channelType, {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) async {
|
||||
final filename = file.path?.split('/')?.last;
|
||||
final mimeType = filename.mimeType;
|
||||
final res = await _channel.sendImage(
|
||||
final res = await _client.sendImage(
|
||||
await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
),
|
||||
channelId,
|
||||
channelType,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return res.file;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> uploadFile(
|
||||
PlatformFile file, {
|
||||
AttachmentFile file,
|
||||
String channelId,
|
||||
String channelType, {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) async {
|
||||
final filename = file.path?.split('/')?.last;
|
||||
final mimeType = filename.mimeType;
|
||||
final res = await _channel.sendFile(
|
||||
final res = await _client.sendFile(
|
||||
await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
),
|
||||
channelId,
|
||||
channelType,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return res.file;
|
||||
}
|
||||
@@ -2,15 +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/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 'package:uuid/uuid.dart';
|
||||
|
||||
import '../client.dart';
|
||||
import '../models/event.dart';
|
||||
@@ -171,19 +172,148 @@ class Channel {
|
||||
/// Call [watch] to initialize the client or instantiate it using [Channel.fromState]
|
||||
Future<bool> get initialized => _initializedCompleter.future;
|
||||
|
||||
/// Send a message to this channel
|
||||
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.
|
||||
///
|
||||
/// Optionally, provide a [reason] for the cancellation.
|
||||
void cancelAttachmentUpload(
|
||||
String attachmentId, {
|
||||
String reason,
|
||||
}) {
|
||||
final cancelToken = _cancelableAttachmentUploadRequest[attachmentId];
|
||||
if (cancelToken == null) {
|
||||
throw Exception(
|
||||
"Upload request for this Attachment hasn't started yet or else Already completed",
|
||||
);
|
||||
}
|
||||
if (cancelToken.isCancelled) throw Exception('Already cancelled');
|
||||
cancelToken.cancel(reason);
|
||||
}
|
||||
|
||||
/// Retries the failed [attachmentId] upload request.
|
||||
Future<void> retryAttachmentUpload(String messageId, String attachmentId) {
|
||||
return _uploadAttachments(messageId, [attachmentId]);
|
||||
}
|
||||
|
||||
Future<void> _uploadAttachments(
|
||||
String messageId,
|
||||
Iterable<String> attachmentIds,
|
||||
) {
|
||||
var message = state.messages.firstWhere(
|
||||
(it) => it.id == messageId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (message == null) {
|
||||
throw Exception('Error, Message not found');
|
||||
}
|
||||
|
||||
final attachments = message.attachments.where((it) {
|
||||
if (it.uploadState.isSuccess) return false;
|
||||
return attachmentIds.contains(it.id);
|
||||
});
|
||||
|
||||
if (attachments.isEmpty) {
|
||||
client.logger.info('No attachments available to upload');
|
||||
if (message.attachments.every((it) => it.uploadState.isSuccess)) {
|
||||
_messageAttachmentsUploadCompleter.remove(messageId)?.complete(message);
|
||||
}
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
client.logger.info('Found ${attachments.length} attachments');
|
||||
return Future.wait(attachments.map((it) {
|
||||
client.logger.info('Uploading ${it.id} attachment...');
|
||||
|
||||
void updateAttachment(Attachment attachment) {
|
||||
message = message.copyWith(
|
||||
attachments: message.attachments.map((it) {
|
||||
if (it.id != attachment.id) return it;
|
||||
return attachment;
|
||||
}).toList(growable: false));
|
||||
state?.addMessage(message);
|
||||
}
|
||||
|
||||
void onSendProgress(int sent, int total) {
|
||||
updateAttachment(it.copyWith(
|
||||
uploadState: UploadState.inProgress(uploaded: sent, total: total),
|
||||
));
|
||||
}
|
||||
|
||||
final isImage = it.type == 'image';
|
||||
final uploader = _client.attachmentUploader;
|
||||
final cancelToken = CancelToken();
|
||||
Future<String> future;
|
||||
if (isImage) {
|
||||
future = uploader.uploadImage(it.file, id, type,
|
||||
onSendProgress: onSendProgress, cancelToken: cancelToken);
|
||||
} else {
|
||||
future = uploader.uploadFile(it.file, id, type,
|
||||
onSendProgress: onSendProgress, cancelToken: cancelToken);
|
||||
}
|
||||
_cancelableAttachmentUploadRequest[it.id] = cancelToken;
|
||||
return future.then((url) {
|
||||
client.logger.info('Attachment ${it.id} uploaded successfully...');
|
||||
if (isImage) {
|
||||
updateAttachment(
|
||||
it.copyWith(imageUrl: url, uploadState: UploadState.success()),
|
||||
);
|
||||
} else {
|
||||
updateAttachment(
|
||||
it.copyWith(assetUrl: url, uploadState: UploadState.success()),
|
||||
);
|
||||
}
|
||||
}).catchError((e, stk) {
|
||||
updateAttachment(
|
||||
it.copyWith(uploadState: UploadState.failed(error: e.toString())),
|
||||
);
|
||||
}).whenComplete(() {
|
||||
_cancelableAttachmentUploadRequest.remove(it.id);
|
||||
});
|
||||
})).whenComplete(() {
|
||||
if (message.attachments.every((it) => it.uploadState.isSuccess)) {
|
||||
_messageAttachmentsUploadCompleter.remove(messageId)?.complete(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Send a [message] to this channel. Optionally pass a [attachmentUploader]
|
||||
/// for custom attachments upload.
|
||||
///
|
||||
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
||||
/// before actually sending the message.
|
||||
Future<SendMessageResponse> sendMessage(Message message) async {
|
||||
final messageId = message.id ?? Uuid().v4();
|
||||
// Cancelling previous completer in case it's called again in the process
|
||||
// Eg. Updating the message while the previous call is in progress.
|
||||
_messageAttachmentsUploadCompleter
|
||||
.remove(message.id)
|
||||
?.completeError('Message Cancelled');
|
||||
|
||||
final quotedMessage = state?.messages?.firstWhere(
|
||||
(m) => m.id == message?.quotedMessageId,
|
||||
orElse: () => null,
|
||||
);
|
||||
final newMessage = message.copyWith(
|
||||
message = message.copyWith(
|
||||
createdAt: message.createdAt ?? DateTime.now(),
|
||||
user: _client.state.user,
|
||||
id: messageId,
|
||||
quotedMessage: quotedMessage,
|
||||
status: MessageSendingStatus.sending,
|
||||
attachments: [
|
||||
...message.attachments.map(
|
||||
(it) {
|
||||
if (it.uploadState.isSuccess) return it;
|
||||
return it.copyWith(
|
||||
uploadState: UploadState.inProgress(
|
||||
uploaded: 0,
|
||||
total: it.file?.size ?? it.extraData['file_size'],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
if (message.parentId != null && message.id == null) {
|
||||
@@ -195,17 +325,24 @@ class Channel {
|
||||
));
|
||||
}
|
||||
|
||||
state?.addMessage(newMessage);
|
||||
state?.addMessage(message);
|
||||
|
||||
try {
|
||||
final attachmentsUploadCompleter = Completer<Message>();
|
||||
_messageAttachmentsUploadCompleter[message.id] =
|
||||
attachmentsUploadCompleter;
|
||||
|
||||
unawaited(_uploadAttachments(
|
||||
message.id,
|
||||
message.attachments.map((it) => it.id),
|
||||
));
|
||||
|
||||
message = await attachmentsUploadCompleter.future;
|
||||
|
||||
final response = await _client.post(
|
||||
'$_channelURL/message',
|
||||
data: {
|
||||
'message': message
|
||||
.copyWith(
|
||||
id: messageId,
|
||||
)
|
||||
.toJson()
|
||||
'message': message.toJson(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -216,7 +353,104 @@ class Channel {
|
||||
return res;
|
||||
} catch (error) {
|
||||
if (error is DioError && error.type != DioErrorType.RESPONSE) {
|
||||
state?.retryQueue?.add([newMessage]);
|
||||
state?.retryQueue?.add([message]);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the [message] in this channel. Optionally pass a [attachmentUploader]
|
||||
/// for custom attachments upload.
|
||||
///
|
||||
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
||||
/// before actually updating the message.
|
||||
Future<UpdateMessageResponse> updateMessage(Message message) async {
|
||||
// Cancelling previous completer in case it's called again in the process
|
||||
// Eg. Updating the message while the previous call is in progress.
|
||||
_messageAttachmentsUploadCompleter
|
||||
.remove(message.id)
|
||||
?.completeError('Message Cancelled');
|
||||
|
||||
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.inProgress(
|
||||
uploaded: 0,
|
||||
total: it.file?.size ?? it.extraData['file_size'],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
state?.addMessage(message);
|
||||
|
||||
try {
|
||||
final attachmentsUploadCompleter = Completer<Message>();
|
||||
_messageAttachmentsUploadCompleter[message.id] =
|
||||
attachmentsUploadCompleter;
|
||||
|
||||
unawaited(_uploadAttachments(
|
||||
message.id,
|
||||
message.attachments.map((it) => it.id),
|
||||
));
|
||||
|
||||
message = await attachmentsUploadCompleter.future;
|
||||
|
||||
final response = await _client.updateMessage(message);
|
||||
state?.addMessage(response?.message?.copyWith(
|
||||
ownReactions: message.ownReactions,
|
||||
));
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error is DioError && error.type != DioErrorType.RESPONSE) {
|
||||
state?.retryQueue?.add([message]);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes the [message] from the channel.
|
||||
Future<EmptyResponse> deleteMessage(Message message) async {
|
||||
// Directly deleting the local messages which are not yet sent to server
|
||||
if (message.status == MessageSendingStatus.sending ||
|
||||
message.status == MessageSendingStatus.failed) {
|
||||
state.addMessage(message.copyWith(
|
||||
type: 'deleted',
|
||||
status: MessageSendingStatus.sent,
|
||||
));
|
||||
|
||||
// Removing the attachments upload completer to stop the `sendMessage`
|
||||
// waiting for attachments to complete.
|
||||
_messageAttachmentsUploadCompleter
|
||||
.remove(message.id)
|
||||
?.completeError(Exception('Message deleted'));
|
||||
return EmptyResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
message = message.copyWith(
|
||||
type: 'deleted',
|
||||
status: MessageSendingStatus.deleting,
|
||||
deletedAt: message.deletedAt ?? DateTime.now(),
|
||||
);
|
||||
|
||||
state?.addMessage(message);
|
||||
|
||||
final response = await _client.deleteMessage(message);
|
||||
|
||||
state?.addMessage(message.copyWith(status: MessageSendingStatus.sent));
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error is DioError && error.type != DioErrorType.RESPONSE) {
|
||||
state?.retryQueue?.add([message]);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
@@ -226,26 +460,30 @@ class Channel {
|
||||
Future<SendFileResponse> sendFile(
|
||||
MultipartFile file, {
|
||||
ProgressCallback onSendProgress,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
'$_channelURL/file',
|
||||
data: FormData.fromMap({'file': file}),
|
||||
CancelToken cancelToken,
|
||||
}) {
|
||||
return _client.sendFile(
|
||||
file,
|
||||
id,
|
||||
type,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return _client.decode(response.data, SendFileResponse.fromJson);
|
||||
}
|
||||
|
||||
/// Send an image to this channel
|
||||
Future<SendImageResponse> sendImage(
|
||||
MultipartFile file, {
|
||||
MultipartFile image, {
|
||||
ProgressCallback onSendProgress,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
'$_channelURL/image',
|
||||
data: FormData.fromMap({'file': file}),
|
||||
CancelToken cancelToken,
|
||||
}) {
|
||||
return _client.sendImage(
|
||||
image,
|
||||
id,
|
||||
type,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return _client.decode(response.data, SendImageResponse.fromJson);
|
||||
}
|
||||
|
||||
/// Delete a file from this channel
|
||||
@@ -975,7 +1213,13 @@ class ChannelClientState {
|
||||
?.getChannelThreads(_channel.cid)
|
||||
?.then((threads) {
|
||||
_threads = threads;
|
||||
retryFailedMessages();
|
||||
})?.then((_) {
|
||||
_channel._client.chatPersistenceClient
|
||||
?.getChannelStateByCid(_channel.cid)
|
||||
?.then((state) {
|
||||
updateChannelState(state);
|
||||
retryFailedMessages();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -125,10 +125,7 @@ class RetryQueue {
|
||||
Future<void> _sendMessage(Message message) async {
|
||||
if (message.status == MessageSendingStatus.failed_update ||
|
||||
message.status == MessageSendingStatus.updating) {
|
||||
await channel.client.updateMessage(
|
||||
message,
|
||||
channel.cid,
|
||||
);
|
||||
await channel.updateMessage(message);
|
||||
} else if (message.status == MessageSendingStatus.failed ||
|
||||
message.status == MessageSendingStatus.sending) {
|
||||
await channel.sendMessage(
|
||||
@@ -136,10 +133,7 @@ class RetryQueue {
|
||||
);
|
||||
} else if (message.status == MessageSendingStatus.failed_delete ||
|
||||
message.status == MessageSendingStatus.deleting) {
|
||||
await channel.client.deleteMessage(
|
||||
message,
|
||||
channel.cid,
|
||||
);
|
||||
await channel.client.deleteMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:stream_chat/src/models/own_user.dart';
|
||||
import 'package:stream_chat/version.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'api/attachment_uploader.dart';
|
||||
import 'api/channel.dart';
|
||||
import 'api/connection_status.dart';
|
||||
import 'api/requests.dart';
|
||||
@@ -78,6 +79,7 @@ class StreamChatClient {
|
||||
Duration receiveTimeout = const Duration(seconds: 6),
|
||||
Dio httpClient,
|
||||
RetryPolicy retryPolicy,
|
||||
this.attachmentUploader,
|
||||
}) {
|
||||
_retryPolicy ??= RetryPolicy(
|
||||
retryTimeout: (StreamChatClient client, int attempt, ApiError error) =>
|
||||
@@ -86,6 +88,8 @@ class StreamChatClient {
|
||||
attempt < 5,
|
||||
);
|
||||
|
||||
attachmentUploader ??= StreamAttachmentUploader(this);
|
||||
|
||||
state = ClientState(this);
|
||||
|
||||
_setupLogger();
|
||||
@@ -97,6 +101,9 @@ class StreamChatClient {
|
||||
/// Chat persistence client
|
||||
ChatPersistenceClient chatPersistenceClient;
|
||||
|
||||
/// Attachment uploader
|
||||
AttachmentUploader attachmentUploader;
|
||||
|
||||
/// Whether the chat persistence is available or not
|
||||
bool get persistenceEnabled => chatPersistenceClient != null;
|
||||
|
||||
@@ -795,12 +802,14 @@ class StreamChatClient {
|
||||
String path, {
|
||||
dynamic data,
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await httpClient.post<String>(
|
||||
path,
|
||||
data: data,
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return response;
|
||||
} on DioError catch (error) {
|
||||
@@ -1030,6 +1039,40 @@ class StreamChatClient {
|
||||
response.data, SearchMessagesResponse.fromJson);
|
||||
}
|
||||
|
||||
/// Send a [file] to the [channelId] of type [channelType]
|
||||
Future<SendFileResponse> sendFile(
|
||||
MultipartFile file,
|
||||
String channelId,
|
||||
String channelType, {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) async {
|
||||
final response = await post(
|
||||
'/channels/$channelType/$channelId/file',
|
||||
data: FormData.fromMap({'file': file}),
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return decode(response.data, SendFileResponse.fromJson);
|
||||
}
|
||||
|
||||
/// Send a [image] to the [channelId] of type [channelType]
|
||||
Future<SendImageResponse> sendImage(
|
||||
MultipartFile image,
|
||||
String channelId,
|
||||
String channelType, {
|
||||
ProgressCallback onSendProgress,
|
||||
CancelToken cancelToken,
|
||||
}) async {
|
||||
final response = await post(
|
||||
'/channels/$channelType/$channelId/image',
|
||||
data: FormData.fromMap({'file': image}),
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
return decode(response.data, SendImageResponse.fromJson);
|
||||
}
|
||||
|
||||
/// Add a device for Push Notifications.
|
||||
Future<EmptyResponse> addDevice(String id, PushProvider pushProvider) async {
|
||||
final response = await post('/devices', data: {
|
||||
@@ -1202,77 +1245,18 @@ class StreamChatClient {
|
||||
}
|
||||
|
||||
/// Update the given message
|
||||
Future<UpdateMessageResponse> updateMessage(
|
||||
Message message, [
|
||||
String cid,
|
||||
]) async {
|
||||
message = message.copyWith(
|
||||
status: MessageSendingStatus.updating,
|
||||
updatedAt: message.updatedAt ?? DateTime.now(),
|
||||
Future<UpdateMessageResponse> updateMessage(Message message) async {
|
||||
final response = await post(
|
||||
'/messages/${message.id}',
|
||||
data: {'message': message},
|
||||
);
|
||||
|
||||
final channel = state?.channels != null ? state?.channels[cid] : null;
|
||||
channel?.state?.addMessage(message);
|
||||
|
||||
return post('/messages/${message.id}', data: {'message': message})
|
||||
.then((res) {
|
||||
final updateMessageResponse = decode(
|
||||
res?.data,
|
||||
UpdateMessageResponse.fromJson,
|
||||
);
|
||||
|
||||
channel?.state?.addMessage(updateMessageResponse?.message?.copyWith(
|
||||
ownReactions: message.ownReactions,
|
||||
));
|
||||
|
||||
return updateMessageResponse;
|
||||
}).catchError((error) {
|
||||
if (error is DioError &&
|
||||
error.type != DioErrorType.RESPONSE &&
|
||||
state?.channels != null) {
|
||||
channel?.state?.retryQueue?.add([message]);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return decode(response.data, UpdateMessageResponse.fromJson);
|
||||
}
|
||||
|
||||
/// Deletes the given message
|
||||
Future<EmptyResponse> deleteMessage(Message message, [String cid]) async {
|
||||
if (message.status == MessageSendingStatus.failed) {
|
||||
state.channels[cid].state.addMessage(message.copyWith(
|
||||
type: 'deleted',
|
||||
status: MessageSendingStatus.sent,
|
||||
));
|
||||
return EmptyResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
message = message.copyWith(
|
||||
type: 'deleted',
|
||||
status: MessageSendingStatus.deleting,
|
||||
deletedAt: message.deletedAt ?? DateTime.now(),
|
||||
);
|
||||
|
||||
if (state?.channels != null) {
|
||||
state.channels[cid]?.state?.addMessage(message);
|
||||
}
|
||||
|
||||
final response = await delete('/messages/${message.id}');
|
||||
|
||||
if (state?.channels != null) {
|
||||
state.channels[cid]?.state
|
||||
?.addMessage(message.copyWith(status: MessageSendingStatus.sent));
|
||||
}
|
||||
|
||||
return decode(response.data, EmptyResponse.fromJson);
|
||||
} catch (error) {
|
||||
if (error is DioError &&
|
||||
error.type != DioErrorType.RESPONSE &&
|
||||
state?.channels != null) {
|
||||
state.channels[cid]?.state?.retryQueue?.add([message]);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
Future<EmptyResponse> deleteMessage(Message message) async {
|
||||
final response = await delete('/messages/${message.id}');
|
||||
return decode(response.data, EmptyResponse.fromJson);
|
||||
}
|
||||
|
||||
/// Get a message by id
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:http_parser/http_parser.dart' as http_parser;
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
/// Useful extension functions for [String]
|
||||
extension StringX on String {
|
||||
/// Returns the mime type from the passed file name.
|
||||
http_parser.MediaType get mimeType {
|
||||
if (this == null) return null;
|
||||
if (toLowerCase().endsWith('heic')) {
|
||||
return http_parser.MediaType.parse('image/heic');
|
||||
} else {
|
||||
return http_parser.MediaType.parse(lookupMimeType(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/models/attachment_file.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'action.dart';
|
||||
import 'serialization.dart';
|
||||
@@ -52,10 +54,21 @@ class Attachment {
|
||||
|
||||
final Uri localUri;
|
||||
|
||||
/// The file present inside this attachment.
|
||||
final AttachmentFile file;
|
||||
|
||||
/// The current upload state of the attachment
|
||||
final UploadState uploadState;
|
||||
|
||||
/// Map of custom channel extraData
|
||||
@JsonKey(includeIfNull: false)
|
||||
final Map<String, dynamic> extraData;
|
||||
|
||||
/// The attachment ID.
|
||||
///
|
||||
/// This is created locally for uniquely identifying a attachment.
|
||||
final String id;
|
||||
|
||||
/// Known top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
static const topLevelFields = [
|
||||
@@ -79,11 +92,20 @@ class Attachment {
|
||||
'actions',
|
||||
];
|
||||
|
||||
/// Known db specific top level fields.
|
||||
/// Useful for [Serialization] methods.
|
||||
static const dbSpecificTopLevelFields = [
|
||||
'id',
|
||||
'upload_state',
|
||||
'file',
|
||||
];
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Attachment({
|
||||
String id,
|
||||
this.type,
|
||||
this.titleLink,
|
||||
this.title,
|
||||
String title,
|
||||
this.thumbUrl,
|
||||
this.text,
|
||||
this.pretext,
|
||||
@@ -100,8 +122,11 @@ class Attachment {
|
||||
this.assetUrl,
|
||||
this.actions,
|
||||
this.extraData,
|
||||
this.localUri,
|
||||
});
|
||||
this.file,
|
||||
this.uploadState,
|
||||
}) : id = id ?? Uuid().v4(),
|
||||
title = title ?? file?.name,
|
||||
localUri = file?.path != null ? Uri.parse(file.path) : null;
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Attachment.fromJson(Map<String, dynamic> json) {
|
||||
@@ -111,9 +136,21 @@ class Attachment {
|
||||
|
||||
/// Serialize to json
|
||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
||||
_$AttachmentToJson(this), topLevelFields);
|
||||
_$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);
|
||||
|
||||
Attachment copyWith({
|
||||
String id,
|
||||
String type,
|
||||
String titleLink,
|
||||
String title,
|
||||
@@ -132,10 +169,12 @@ class Attachment {
|
||||
String authorIcon,
|
||||
String assetUrl,
|
||||
List<Action> actions,
|
||||
Uri localUri,
|
||||
AttachmentFile file,
|
||||
UploadState uploadState,
|
||||
Map<String, dynamic> extraData,
|
||||
}) =>
|
||||
Attachment(
|
||||
id: id ?? this.id,
|
||||
type: type ?? this.type,
|
||||
titleLink: titleLink ?? this.titleLink,
|
||||
title: title ?? this.title,
|
||||
@@ -154,7 +193,8 @@ class Attachment {
|
||||
authorIcon: authorIcon ?? this.authorIcon,
|
||||
assetUrl: assetUrl ?? this.assetUrl,
|
||||
actions: actions ?? this.actions,
|
||||
localUri: localUri ?? this.localUri,
|
||||
file: file ?? this.file,
|
||||
uploadState: uploadState ?? this.uploadState,
|
||||
extraData: extraData ?? this.extraData,
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ part of 'attachment.dart';
|
||||
|
||||
Attachment _$AttachmentFromJson(Map json) {
|
||||
return Attachment(
|
||||
id: json['id'] as String,
|
||||
type: json['type'] as String,
|
||||
titleLink: json['title_link'] as String,
|
||||
title: json['title'] as String,
|
||||
@@ -35,9 +36,16 @@ Attachment _$AttachmentFromJson(Map json) {
|
||||
extraData: (json['extra_data'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
),
|
||||
localUri: json['local_uri'] == null
|
||||
file: json['file'] == null
|
||||
? null
|
||||
: Uri.parse(json['local_uri'] as String),
|
||||
: AttachmentFile.fromJson((json['file'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)),
|
||||
uploadState: json['upload_state'] == null
|
||||
? null
|
||||
: UploadState.fromJson((json['upload_state'] as Map)?.map(
|
||||
(k, e) => MapEntry(k as String, e),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +76,9 @@ Map<String, dynamic> _$AttachmentToJson(Attachment instance) {
|
||||
writeNotNull('author_icon', instance.authorIcon);
|
||||
writeNotNull('asset_url', instance.assetUrl);
|
||||
writeNotNull('actions', instance.actions?.map((e) => e?.toJson())?.toList());
|
||||
writeNotNull('local_uri', instance.localUri?.toString());
|
||||
writeNotNull('file', instance.file?.toJson());
|
||||
writeNotNull('upload_state', instance.uploadState?.toJson());
|
||||
writeNotNull('extra_data', instance.extraData);
|
||||
writeNotNull('id', instance.id);
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'attachment_file.freezed.dart';
|
||||
|
||||
part 'attachment_file.g.dart';
|
||||
|
||||
///
|
||||
@freezed
|
||||
abstract class UploadState with _$UploadState {
|
||||
///
|
||||
const factory UploadState.inProgress({int uploaded, int total}) = InProgress;
|
||||
|
||||
///
|
||||
const factory UploadState.success() = Success;
|
||||
|
||||
///
|
||||
const factory UploadState.failed({@required String error}) = Failed;
|
||||
|
||||
/// Creates a new instance from a json
|
||||
factory UploadState.fromJson(Map<String, dynamic> json) =>
|
||||
_$UploadStateFromJson(json);
|
||||
}
|
||||
|
||||
///
|
||||
extension UploadStateX on UploadState {
|
||||
///
|
||||
bool get isInProgress => this is InProgress;
|
||||
|
||||
///
|
||||
bool get isSuccess => this is Success;
|
||||
|
||||
///
|
||||
bool get isFailed => this is Failed;
|
||||
}
|
||||
|
||||
Uint8List _fromString(String bytes) => Uint8List.fromList(bytes.codeUnits);
|
||||
|
||||
String _toString(Uint8List bytes) => String.fromCharCodes(bytes);
|
||||
|
||||
///
|
||||
@JsonSerializable()
|
||||
class AttachmentFile {
|
||||
///
|
||||
const AttachmentFile({
|
||||
this.path,
|
||||
this.name,
|
||||
this.bytes,
|
||||
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.
|
||||
/// ```
|
||||
/// final File myFile = File(platformFile.path);
|
||||
/// ```
|
||||
final String path;
|
||||
|
||||
/// 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.
|
||||
@JsonKey(toJson: _toString, fromJson: _fromString)
|
||||
final Uint8List bytes;
|
||||
|
||||
/// The file size in bytes.
|
||||
final int size;
|
||||
|
||||
/// 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);
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies
|
||||
|
||||
part of 'attachment_file.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
UploadState _$UploadStateFromJson(Map<String, dynamic> json) {
|
||||
switch (json['runtimeType'] as String) {
|
||||
case 'inProgress':
|
||||
return InProgress.fromJson(json);
|
||||
case 'success':
|
||||
return Success.fromJson(json);
|
||||
case 'failed':
|
||||
return Failed.fromJson(json);
|
||||
|
||||
default:
|
||||
throw FallThroughError();
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$UploadStateTearOff {
|
||||
const _$UploadStateTearOff();
|
||||
|
||||
// ignore: unused_element
|
||||
InProgress inProgress({int uploaded, int total}) {
|
||||
return InProgress(
|
||||
uploaded: uploaded,
|
||||
total: total,
|
||||
);
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
Success success() {
|
||||
return const Success();
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
Failed failed({@required String error}) {
|
||||
return Failed(
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
UploadState fromJson(Map<String, Object> json) {
|
||||
return UploadState.fromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
// ignore: unused_element
|
||||
const $UploadState = _$UploadStateTearOff();
|
||||
|
||||
/// @nodoc
|
||||
mixin _$UploadState {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object>({
|
||||
@required TResult inProgress(int uploaded, int total),
|
||||
@required TResult success(),
|
||||
@required TResult failed(String error),
|
||||
});
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object>({
|
||||
TResult inProgress(int uploaded, int total),
|
||||
TResult success(),
|
||||
TResult failed(String error),
|
||||
@required TResult orElse(),
|
||||
});
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object>({
|
||||
@required TResult inProgress(InProgress value),
|
||||
@required TResult success(Success value),
|
||||
@required TResult failed(Failed value),
|
||||
});
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object>({
|
||||
TResult inProgress(InProgress value),
|
||||
TResult success(Success value),
|
||||
TResult failed(Failed value),
|
||||
@required TResult orElse(),
|
||||
});
|
||||
Map<String, dynamic> toJson();
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $UploadStateCopyWith<$Res> {
|
||||
factory $UploadStateCopyWith(
|
||||
UploadState value, $Res Function(UploadState) then) =
|
||||
_$UploadStateCopyWithImpl<$Res>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$UploadStateCopyWithImpl<$Res> implements $UploadStateCopyWith<$Res> {
|
||||
_$UploadStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
final UploadState _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function(UploadState) _then;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $InProgressCopyWith<$Res> {
|
||||
factory $InProgressCopyWith(
|
||||
InProgress value, $Res Function(InProgress) then) =
|
||||
_$InProgressCopyWithImpl<$Res>;
|
||||
$Res call({int uploaded, int total});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$InProgressCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
|
||||
implements $InProgressCopyWith<$Res> {
|
||||
_$InProgressCopyWithImpl(InProgress _value, $Res Function(InProgress) _then)
|
||||
: super(_value, (v) => _then(v as InProgress));
|
||||
|
||||
@override
|
||||
InProgress get _value => super._value as InProgress;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object uploaded = freezed,
|
||||
Object total = freezed,
|
||||
}) {
|
||||
return _then(InProgress(
|
||||
uploaded: uploaded == freezed ? _value.uploaded : uploaded as int,
|
||||
total: total == freezed ? _value.total : total as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
|
||||
/// @nodoc
|
||||
class _$InProgress implements InProgress {
|
||||
const _$InProgress({this.uploaded, this.total});
|
||||
|
||||
factory _$InProgress.fromJson(Map<String, dynamic> json) =>
|
||||
_$_$InProgressFromJson(json);
|
||||
|
||||
@override
|
||||
final int uploaded;
|
||||
@override
|
||||
final int total;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UploadState.inProgress(uploaded: $uploaded, total: $total)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other is InProgress &&
|
||||
(identical(other.uploaded, uploaded) ||
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.uploaded, uploaded)) &&
|
||||
(identical(other.total, total) ||
|
||||
const DeepCollectionEquality().equals(other.total, total)));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
runtimeType.hashCode ^
|
||||
const DeepCollectionEquality().hash(uploaded) ^
|
||||
const DeepCollectionEquality().hash(total);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
$InProgressCopyWith<InProgress> get copyWith =>
|
||||
_$InProgressCopyWithImpl<InProgress>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object>({
|
||||
@required TResult inProgress(int uploaded, int total),
|
||||
@required TResult success(),
|
||||
@required TResult failed(String error),
|
||||
}) {
|
||||
assert(inProgress != null);
|
||||
assert(success != null);
|
||||
assert(failed != null);
|
||||
return inProgress(uploaded, total);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object>({
|
||||
TResult inProgress(int uploaded, int total),
|
||||
TResult success(),
|
||||
TResult failed(String error),
|
||||
@required TResult orElse(),
|
||||
}) {
|
||||
assert(orElse != null);
|
||||
if (inProgress != null) {
|
||||
return inProgress(uploaded, total);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object>({
|
||||
@required TResult inProgress(InProgress value),
|
||||
@required TResult success(Success value),
|
||||
@required TResult failed(Failed value),
|
||||
}) {
|
||||
assert(inProgress != null);
|
||||
assert(success != null);
|
||||
assert(failed != null);
|
||||
return inProgress(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object>({
|
||||
TResult inProgress(InProgress value),
|
||||
TResult success(Success value),
|
||||
TResult failed(Failed value),
|
||||
@required TResult orElse(),
|
||||
}) {
|
||||
assert(orElse != null);
|
||||
if (inProgress != null) {
|
||||
return inProgress(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$_$InProgressToJson(this)..['runtimeType'] = 'inProgress';
|
||||
}
|
||||
}
|
||||
|
||||
abstract class InProgress implements UploadState {
|
||||
const factory InProgress({int uploaded, int total}) = _$InProgress;
|
||||
|
||||
factory InProgress.fromJson(Map<String, dynamic> json) =
|
||||
_$InProgress.fromJson;
|
||||
|
||||
int get uploaded;
|
||||
int get total;
|
||||
@JsonKey(ignore: true)
|
||||
$InProgressCopyWith<InProgress> get copyWith;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SuccessCopyWith<$Res> {
|
||||
factory $SuccessCopyWith(Success value, $Res Function(Success) then) =
|
||||
_$SuccessCopyWithImpl<$Res>;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SuccessCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
|
||||
implements $SuccessCopyWith<$Res> {
|
||||
_$SuccessCopyWithImpl(Success _value, $Res Function(Success) _then)
|
||||
: super(_value, (v) => _then(v as Success));
|
||||
|
||||
@override
|
||||
Success get _value => super._value as Success;
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
|
||||
/// @nodoc
|
||||
class _$Success implements Success {
|
||||
const _$Success();
|
||||
|
||||
factory _$Success.fromJson(Map<String, dynamic> json) =>
|
||||
_$_$SuccessFromJson(json);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UploadState.success()';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) || (other is Success);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object>({
|
||||
@required TResult inProgress(int uploaded, int total),
|
||||
@required TResult success(),
|
||||
@required TResult failed(String error),
|
||||
}) {
|
||||
assert(inProgress != null);
|
||||
assert(success != null);
|
||||
assert(failed != null);
|
||||
return success();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object>({
|
||||
TResult inProgress(int uploaded, int total),
|
||||
TResult success(),
|
||||
TResult failed(String error),
|
||||
@required TResult orElse(),
|
||||
}) {
|
||||
assert(orElse != null);
|
||||
if (success != null) {
|
||||
return success();
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object>({
|
||||
@required TResult inProgress(InProgress value),
|
||||
@required TResult success(Success value),
|
||||
@required TResult failed(Failed value),
|
||||
}) {
|
||||
assert(inProgress != null);
|
||||
assert(success != null);
|
||||
assert(failed != null);
|
||||
return success(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object>({
|
||||
TResult inProgress(InProgress value),
|
||||
TResult success(Success value),
|
||||
TResult failed(Failed value),
|
||||
@required TResult orElse(),
|
||||
}) {
|
||||
assert(orElse != null);
|
||||
if (success != null) {
|
||||
return success(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$_$SuccessToJson(this)..['runtimeType'] = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Success implements UploadState {
|
||||
const factory Success() = _$Success;
|
||||
|
||||
factory Success.fromJson(Map<String, dynamic> json) = _$Success.fromJson;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $FailedCopyWith<$Res> {
|
||||
factory $FailedCopyWith(Failed value, $Res Function(Failed) then) =
|
||||
_$FailedCopyWithImpl<$Res>;
|
||||
$Res call({String error});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$FailedCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
|
||||
implements $FailedCopyWith<$Res> {
|
||||
_$FailedCopyWithImpl(Failed _value, $Res Function(Failed) _then)
|
||||
: super(_value, (v) => _then(v as Failed));
|
||||
|
||||
@override
|
||||
Failed get _value => super._value as Failed;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object error = freezed,
|
||||
}) {
|
||||
return _then(Failed(
|
||||
error: error == freezed ? _value.error : error as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
|
||||
/// @nodoc
|
||||
class _$Failed implements Failed {
|
||||
const _$Failed({@required this.error}) : assert(error != null);
|
||||
|
||||
factory _$Failed.fromJson(Map<String, dynamic> json) =>
|
||||
_$_$FailedFromJson(json);
|
||||
|
||||
@override
|
||||
final String error;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UploadState.failed(error: $error)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other is Failed &&
|
||||
(identical(other.error, error) ||
|
||||
const DeepCollectionEquality().equals(other.error, error)));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
runtimeType.hashCode ^ const DeepCollectionEquality().hash(error);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
$FailedCopyWith<Failed> get copyWith =>
|
||||
_$FailedCopyWithImpl<Failed>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object>({
|
||||
@required TResult inProgress(int uploaded, int total),
|
||||
@required TResult success(),
|
||||
@required TResult failed(String error),
|
||||
}) {
|
||||
assert(inProgress != null);
|
||||
assert(success != null);
|
||||
assert(failed != null);
|
||||
return failed(error);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object>({
|
||||
TResult inProgress(int uploaded, int total),
|
||||
TResult success(),
|
||||
TResult failed(String error),
|
||||
@required TResult orElse(),
|
||||
}) {
|
||||
assert(orElse != null);
|
||||
if (failed != null) {
|
||||
return failed(error);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object>({
|
||||
@required TResult inProgress(InProgress value),
|
||||
@required TResult success(Success value),
|
||||
@required TResult failed(Failed value),
|
||||
}) {
|
||||
assert(inProgress != null);
|
||||
assert(success != null);
|
||||
assert(failed != null);
|
||||
return failed(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object>({
|
||||
TResult inProgress(InProgress value),
|
||||
TResult success(Success value),
|
||||
TResult failed(Failed value),
|
||||
@required TResult orElse(),
|
||||
}) {
|
||||
assert(orElse != null);
|
||||
if (failed != null) {
|
||||
return failed(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$_$FailedToJson(this)..['runtimeType'] = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Failed implements UploadState {
|
||||
const factory Failed({@required String error}) = _$Failed;
|
||||
|
||||
factory Failed.fromJson(Map<String, dynamic> json) = _$Failed.fromJson;
|
||||
|
||||
String get error;
|
||||
@JsonKey(ignore: true)
|
||||
$FailedCopyWith<Failed> get copyWith;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'attachment_file.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AttachmentFile _$AttachmentFileFromJson(Map json) {
|
||||
return AttachmentFile(
|
||||
path: json['path'] as String,
|
||||
name: json['name'] as String,
|
||||
bytes: _fromString(json['bytes'] as String),
|
||||
size: json['size'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$AttachmentFileToJson(AttachmentFile instance) =>
|
||||
<String, dynamic>{
|
||||
'path': instance.path,
|
||||
'name': instance.name,
|
||||
'bytes': _toString(instance.bytes),
|
||||
'size': instance.size,
|
||||
};
|
||||
|
||||
_$InProgress _$_$InProgressFromJson(Map json) {
|
||||
return _$InProgress(
|
||||
uploaded: json['uploaded'] as int,
|
||||
total: json['total'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$_$InProgressToJson(_$InProgress instance) =>
|
||||
<String, dynamic>{
|
||||
'uploaded': instance.uploaded,
|
||||
'total': instance.total,
|
||||
};
|
||||
|
||||
_$Success _$_$SuccessFromJson(Map json) {
|
||||
return _$Success();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$_$SuccessToJson(_$Success instance) =>
|
||||
<String, dynamic>{};
|
||||
|
||||
_$Failed _$_$FailedFromJson(Map json) {
|
||||
return _$Failed(
|
||||
error: json['error'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$_$FailedToJson(_$Failed instance) => <String, dynamic>{
|
||||
'error': instance.error,
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'attachment.dart';
|
||||
import 'reaction.dart';
|
||||
@@ -163,7 +164,7 @@ class Message {
|
||||
|
||||
/// Constructor used for json serialization
|
||||
Message({
|
||||
this.id,
|
||||
String id,
|
||||
this.text,
|
||||
this.type,
|
||||
this.attachments,
|
||||
@@ -187,7 +188,7 @@ class Message {
|
||||
this.extraData,
|
||||
this.deletedAt,
|
||||
this.status = MessageSendingStatus.sent,
|
||||
});
|
||||
}) : id = id ?? Uuid().v4();
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
|
||||
|
||||
@@ -10,10 +10,12 @@ export './src/api/connection_status.dart';
|
||||
export './src/api/requests.dart';
|
||||
export './src/api/requests.dart';
|
||||
export './src/api/responses.dart';
|
||||
export './src/api/attachment_uploader.dart' show AttachmentUploader;
|
||||
export './src/client.dart';
|
||||
export './src/event_type.dart';
|
||||
export './src/models/action.dart';
|
||||
export './src/models/attachment.dart';
|
||||
export './src/models/attachment_file.dart';
|
||||
export './src/models/channel_config.dart';
|
||||
export './src/models/channel_model.dart';
|
||||
export './src/models/channel_state.dart';
|
||||
@@ -27,4 +29,5 @@ 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';
|
||||
|
||||
@@ -19,9 +19,11 @@ dependencies:
|
||||
collection: ^1.14.13
|
||||
pedantic: ^1.9.2
|
||||
meta: ^1.2.4
|
||||
freezed_annotation: ^0.12.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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
1589B944F51366A883B3A7A5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AFDB673F1C9808CE4EC418F /* Pods_Runner.framework */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
@@ -31,7 +32,11 @@
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
24CE22BB301621B9BF1A7A7C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
4684439012E1DB1A82103E26 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
4AFDB673F1C9808CE4EC418F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
59062C6EC2CCFE110AC70AB8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -49,12 +54,24 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1589B944F51366A883B3A7A5 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
38CA51382F78F25FC59B6C81 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
24CE22BB301621B9BF1A7A7C /* Pods-Runner.debug.xcconfig */,
|
||||
59062C6EC2CCFE110AC70AB8 /* Pods-Runner.release.xcconfig */,
|
||||
4684439012E1DB1A82103E26 /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -72,6 +89,8 @@
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
38CA51382F78F25FC59B6C81 /* Pods */,
|
||||
E3E00C71A36D1ABA459667BF /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -98,6 +117,14 @@
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E3E00C71A36D1ABA459667BF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4AFDB673F1C9808CE4EC418F /* Pods_Runner.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -105,12 +132,14 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
41D6CB24535AC541800DBB07 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
9DF0031578B883CBE79BDCC8 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -183,6 +212,28 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
41D6CB24535AC541800DBB07 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -197,6 +248,23 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
9DF0031578B883CBE79BDCC8 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
||||
+3
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
|
||||
|
||||
final chatPersistentClient = StreamChatPersistenceClient(
|
||||
logLevel: Level.INFO,
|
||||
connectionMode: ConnectionMode.background,
|
||||
);
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
/// Create a new instance of [StreamChatClient] passing the apikey obtained from your
|
||||
/// project dashboard.
|
||||
final client = StreamChatClient(
|
||||
's2dxdhpxd94g',
|
||||
logLevel: Level.INFO,
|
||||
);
|
||||
)..chatPersistenceClient = chatPersistentClient;
|
||||
|
||||
/// Set the current user and connect the websocket. In a production scenario, this should be done using
|
||||
/// a backend to generate a user token using our server SDK.
|
||||
@@ -20,8 +28,7 @@ void main() async {
|
||||
|
||||
final channel = client.channel('messaging', id: 'godevs');
|
||||
|
||||
// ignore: unawaited_futures
|
||||
channel.watch();
|
||||
await channel.watch();
|
||||
|
||||
runApp(MyApp(client, channel));
|
||||
}
|
||||
@@ -49,6 +56,9 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
themeMode: ThemeMode.system,
|
||||
builder: (context, widget) {
|
||||
return StreamChat(
|
||||
child: widget,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export 'file_attachment.dart';
|
||||
export 'giphy_attachment.dart';
|
||||
export 'image_attachment.dart';
|
||||
export 'video_attachment.dart';
|
||||
export 'attachment_widget.dart'
|
||||
show AttachmentError, AttachmentSource, AttachmentSourceX;
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'utils.dart';
|
||||
import '../stream_chat_theme.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
class AttachmentTitle extends StatelessWidget {
|
||||
const AttachmentTitle({
|
||||
@@ -0,0 +1,197 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
typedef InProgressBuilder = Widget Function(BuildContext, int, int);
|
||||
typedef FailedBuilder = Widget Function(BuildContext, String);
|
||||
|
||||
class AttachmentUploadStateBuilder extends StatelessWidget {
|
||||
final Message message;
|
||||
final Attachment attachment;
|
||||
final FailedBuilder failedBuilder;
|
||||
final WidgetBuilder successBuilder;
|
||||
final InProgressBuilder inProgressBuilder;
|
||||
|
||||
const AttachmentUploadStateBuilder({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.attachment,
|
||||
this.failedBuilder,
|
||||
this.successBuilder,
|
||||
this.inProgressBuilder,
|
||||
}) : assert(message != null),
|
||||
assert(attachment != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.uploadState == null) return Offstage();
|
||||
|
||||
final messageId = message.id;
|
||||
final attachmentId = attachment.id;
|
||||
|
||||
var inProgress = inProgressBuilder;
|
||||
inProgress ??= (context, int sent, int total) {
|
||||
return _InProgressState(
|
||||
sent: sent,
|
||||
total: total,
|
||||
attachmentId: attachmentId,
|
||||
);
|
||||
};
|
||||
|
||||
var failed = failedBuilder;
|
||||
failed ??= (context, error) {
|
||||
return _FailedState(
|
||||
error: error,
|
||||
messageId: messageId,
|
||||
attachmentId: attachmentId,
|
||||
);
|
||||
};
|
||||
|
||||
var success = successBuilder;
|
||||
success ??= (context) => _SuccessState();
|
||||
|
||||
return attachment.uploadState.when(
|
||||
inProgress: (sent, total) => inProgress(context, sent, total),
|
||||
success: () => success(context),
|
||||
failed: (error) => failed(context, error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IconButton extends StatelessWidget {
|
||||
final Widget icon;
|
||||
final double iconSize;
|
||||
final VoidCallback onPressed;
|
||||
final Color fillColor;
|
||||
|
||||
const _IconButton({
|
||||
Key key,
|
||||
this.icon,
|
||||
this.iconSize = 24.0,
|
||||
this.onPressed,
|
||||
this.fillColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
child: RawMaterialButton(
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: onPressed,
|
||||
fillColor:
|
||||
fillColor ?? StreamChatTheme.of(context).colorTheme.overlayDark,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InProgressState extends StatelessWidget {
|
||||
final int sent;
|
||||
final int total;
|
||||
final String attachmentId;
|
||||
|
||||
const _InProgressState({
|
||||
Key key,
|
||||
@required this.sent,
|
||||
@required this.total,
|
||||
@required this.attachmentId,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_IconButton(
|
||||
icon: StreamSvgIcon.close(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
|
||||
),
|
||||
Center(
|
||||
child: UploadProgressIndicator(
|
||||
uploaded: sent,
|
||||
total: total,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FailedState extends StatelessWidget {
|
||||
final String error;
|
||||
final String messageId;
|
||||
final String attachmentId;
|
||||
|
||||
const _FailedState({
|
||||
Key key,
|
||||
this.error,
|
||||
@required this.messageId,
|
||||
@required this.attachmentId,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_IconButton(
|
||||
icon: StreamSvgIcon.retry(
|
||||
color: theme.colorTheme.white,
|
||||
),
|
||||
onPressed: () {
|
||||
return channel.retryAttachmentUpload(messageId, attachmentId);
|
||||
},
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorTheme.overlayDark.withOpacity(0.6),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
child: Text(
|
||||
'UPLOAD ERROR',
|
||||
style: theme.textTheme.footnote.copyWith(
|
||||
color: theme.colorTheme.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SuccessState extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark,
|
||||
maxRadius: 12.0,
|
||||
child: StreamSvgIcon.check(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import '../stream_chat_theme.dart';
|
||||
|
||||
enum AttachmentSource {
|
||||
local,
|
||||
network,
|
||||
}
|
||||
|
||||
extension AttachmentSourceX on AttachmentSource {
|
||||
/// The [when] method is the equivalent to pattern matching.
|
||||
/// Its prototype depends on the AttachmentSource defined.
|
||||
T when<T>({
|
||||
@required T Function() local,
|
||||
@required T Function() network,
|
||||
}) {
|
||||
assert(() {
|
||||
if (local == null || network == null) {
|
||||
throw 'check for all possible cases';
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
switch (this) {
|
||||
case AttachmentSource.local:
|
||||
return local();
|
||||
case AttachmentSource.network:
|
||||
return network();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AttachmentWidget extends StatelessWidget {
|
||||
final Size size;
|
||||
final Message message;
|
||||
final Attachment attachment;
|
||||
final AttachmentSource _source;
|
||||
|
||||
AttachmentSource get source => _source ?? attachment.file != null
|
||||
? AttachmentSource.local
|
||||
: AttachmentSource.network;
|
||||
|
||||
const AttachmentWidget({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.attachment,
|
||||
this.size,
|
||||
AttachmentSource source,
|
||||
}) : _source = source,
|
||||
super(key: key);
|
||||
}
|
||||
|
||||
class AttachmentError extends StatelessWidget {
|
||||
final Size size;
|
||||
|
||||
const AttachmentError({
|
||||
Key key,
|
||||
this.size,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import '../upload_progress_indicator.dart';
|
||||
import 'attachment_widget.dart';
|
||||
|
||||
class FileAttachment extends AttachmentWidget {
|
||||
final Widget title;
|
||||
final Widget trailing;
|
||||
|
||||
const FileAttachment({
|
||||
Key key,
|
||||
@required Message message,
|
||||
@required Attachment attachment,
|
||||
Size size,
|
||||
this.title,
|
||||
this.trailing,
|
||||
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||
|
||||
bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video';
|
||||
|
||||
bool get isImageAttachment => attachment.title?.mimeType?.type == 'image';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Container(
|
||||
width: size?.width ?? 100,
|
||||
height: 56.0,
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
child: _getFileTypeImage(context),
|
||||
height: 40.0,
|
||||
width: 33.33,
|
||||
margin: EdgeInsets.all(8.0),
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
attachment?.title ?? 'File',
|
||||
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 3.0),
|
||||
_buildSubtitle(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
_buildTrailing(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ShapeBorder _getDefaultShape(BuildContext context) {
|
||||
return RoundedRectangleBorder(
|
||||
side: BorderSide(width: 0.0, color: Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getFileTypeImage(BuildContext context) {
|
||||
if (isImageAttachment) {
|
||||
return Material(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
type: MaterialType.transparency,
|
||||
shape: _getDefaultShape(context),
|
||||
child: source.when(
|
||||
local: () => Image.memory(
|
||||
attachment.file.bytes,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, obj, trace) {
|
||||
return getFileTypeImage(attachment.extraData['other']);
|
||||
},
|
||||
),
|
||||
network: () => CachedNetworkImage(
|
||||
imageUrl: attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, obj, trace) {
|
||||
return getFileTypeImage(attachment.extraData['other']);
|
||||
},
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isVideoAttachment) {
|
||||
return Material(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
type: MaterialType.transparency,
|
||||
shape: _getDefaultShape(context),
|
||||
child: source.when(
|
||||
local: () => VideoThumbnailImage(
|
||||
video: attachment.file.path,
|
||||
placeholderBuilder: (_) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
network: () => VideoThumbnailImage(
|
||||
video: attachment.assetUrl,
|
||||
placeholderBuilder: (_) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return getFileTypeImage(attachment.extraData['mime_type']);
|
||||
}
|
||||
|
||||
Widget _buildButton({
|
||||
Widget icon,
|
||||
double iconSize = 24.0,
|
||||
VoidCallback onPressed,
|
||||
Color fillColor,
|
||||
}) {
|
||||
return Container(
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
child: RawMaterialButton(
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: onPressed,
|
||||
fillColor: fillColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrailing(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final attachmentId = attachment.id;
|
||||
var trailingWidget = trailing;
|
||||
trailingWidget ??= attachment.uploadState?.when(
|
||||
inProgress: (_, __) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _buildButton(
|
||||
icon: StreamSvgIcon.close(color: theme.colorTheme.white),
|
||||
fillColor: theme.colorTheme.overlayDark,
|
||||
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
|
||||
),
|
||||
),
|
||||
success: () => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: theme.colorTheme.accentBlue,
|
||||
maxRadius: 12.0,
|
||||
child: StreamSvgIcon.check(color: theme.colorTheme.white),
|
||||
),
|
||||
),
|
||||
failed: (_) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _buildButton(
|
||||
icon: StreamSvgIcon.retry(color: theme.colorTheme.white),
|
||||
fillColor: theme.colorTheme.overlayDark,
|
||||
onPressed: () => channel.retryAttachmentUpload(
|
||||
message?.id,
|
||||
attachmentId,
|
||||
),
|
||||
),
|
||||
),
|
||||
) ??
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black),
|
||||
padding: const EdgeInsets.all(8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
splashRadius: 16,
|
||||
onPressed: () {
|
||||
launchURL(context, attachment.assetUrl);
|
||||
},
|
||||
);
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: trailingWidget,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitle(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
final size = attachment.file?.size ?? attachment.extraData['file_size'];
|
||||
final textStyle = theme.textTheme.footnote.copyWith(
|
||||
color: theme.colorTheme.grey,
|
||||
);
|
||||
return attachment.uploadState?.when(
|
||||
inProgress: (sent, total) {
|
||||
return UploadProgressIndicator(
|
||||
uploaded: sent,
|
||||
total: total,
|
||||
showBackground: false,
|
||||
padding: EdgeInsets.zero,
|
||||
textStyle: textStyle,
|
||||
progressIndicatorColor: theme.colorTheme.accentBlue,
|
||||
);
|
||||
},
|
||||
success: () {
|
||||
return Text(
|
||||
'${fileSize(size, 1)}/${fileSize(size, 1)}',
|
||||
style: textStyle,
|
||||
);
|
||||
},
|
||||
failed: (_) => Text('UPLOAD ERROR', style: textStyle),
|
||||
) ??
|
||||
Text('${fileSize(size)}', style: textStyle);
|
||||
}
|
||||
}
|
||||
+52
-75
@@ -1,48 +1,42 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'attachment_error.dart';
|
||||
import 'full_screen_media.dart';
|
||||
import '../full_screen_media.dart';
|
||||
import '../stream_chat_theme.dart';
|
||||
import '../stream_svg_icon.dart';
|
||||
import 'attachment_widget.dart';
|
||||
|
||||
class GiphyAttachment extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
class GiphyAttachment extends AttachmentWidget {
|
||||
final MessageTheme messageTheme;
|
||||
final Message message;
|
||||
final Size size;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
|
||||
const GiphyAttachment({
|
||||
Key key,
|
||||
this.attachment,
|
||||
@required Message message,
|
||||
@required Attachment attachment,
|
||||
Size size,
|
||||
this.messageTheme,
|
||||
this.message,
|
||||
this.size,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
}) : super(key: key);
|
||||
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.thumbUrl == null &&
|
||||
attachment.imageUrl == null &&
|
||||
attachment.assetUrl == null) {
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
);
|
||||
final imageUrl =
|
||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
||||
if (imageUrl == null && source == AttachmentSource.network) {
|
||||
return AttachmentError();
|
||||
}
|
||||
|
||||
return attachment.actions != null
|
||||
? _buildSendingAttachment(context)
|
||||
: _buildSentAttachment(context);
|
||||
if (attachment.actions != null) {
|
||||
return _buildSendingAttachment(context, imageUrl);
|
||||
}
|
||||
return _buildSentAttachment(context, imageUrl);
|
||||
}
|
||||
|
||||
Widget _buildSendingAttachment(context) {
|
||||
Widget _buildSendingAttachment(BuildContext context, String imageUrl) {
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -67,9 +61,7 @@ class GiphyAttachment extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
_onImageTap(context);
|
||||
},
|
||||
onTap: () => _onImageTap(context),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
@@ -87,13 +79,10 @@ class GiphyAttachment extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: attachment.thumbUrl ??
|
||||
attachment.imageUrl ??
|
||||
attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) {
|
||||
return AttachmentError(size: size);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
@@ -305,44 +294,38 @@ class GiphyAttachment extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _onImageTap(BuildContext context) async {
|
||||
var res = await Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [
|
||||
attachment,
|
||||
],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
|
||||
if (res != null) {
|
||||
onReturnAction(res);
|
||||
}
|
||||
final res = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (res != null) onReturnAction(res);
|
||||
}
|
||||
|
||||
Widget _buildSentAttachment(context) {
|
||||
Widget _buildSentAttachment(BuildContext context, String imageUrl) {
|
||||
return Container(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
var res =
|
||||
final res =
|
||||
await Navigator.push(context, MaterialPageRoute(builder: (_) {
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [
|
||||
attachment,
|
||||
],
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
@@ -350,10 +333,7 @@ class GiphyAttachment extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}));
|
||||
|
||||
if (res != null) {
|
||||
onReturnAction(res);
|
||||
}
|
||||
if (res != null) onReturnAction(res);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
@@ -369,13 +349,10 @@ class GiphyAttachment extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: attachment.thumbUrl ??
|
||||
attachment.imageUrl ??
|
||||
attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) {
|
||||
return AttachmentError(size: size);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Positioned(
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import 'attachment_title.dart';
|
||||
import '../full_screen_media.dart';
|
||||
import '../stream_chat_theme.dart';
|
||||
import 'attachment_widget.dart';
|
||||
|
||||
class ImageAttachment extends AttachmentWidget {
|
||||
final MessageTheme messageTheme;
|
||||
final bool showTitle;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
final VoidCallback onAttachmentTap;
|
||||
|
||||
const ImageAttachment({
|
||||
Key key,
|
||||
@required Message message,
|
||||
@required Attachment attachment,
|
||||
Size size,
|
||||
this.messageTheme,
|
||||
this.showTitle = false,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
this.onAttachmentTap,
|
||||
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return source.when(
|
||||
local: () {
|
||||
if (attachment.localUri == null) {
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
return _buildImageAttachment(
|
||||
context,
|
||||
Image.memory(
|
||||
attachment.file.bytes,
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, _, __) {
|
||||
return Image.asset(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
network: () {
|
||||
final imageUrl =
|
||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
||||
if (imageUrl == null) {
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
return _buildImageAttachment(
|
||||
context,
|
||||
CachedNetworkImage(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) {
|
||||
return Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) {
|
||||
return AttachmentError(size: size);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageAttachment(BuildContext context, Widget imageWidget) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: onAttachmentTap ??
|
||||
() async {
|
||||
final result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (result != null) onReturnAction(result);
|
||||
},
|
||||
child: imageWidget,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AttachmentUploadStateBuilder(
|
||||
message: message,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showTitle && attachment.title != null)
|
||||
Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
messageTheme: messageTheme,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'attachment_title.dart';
|
||||
import 'attachment_upload_state_builder.dart';
|
||||
import 'attachment_widget.dart';
|
||||
|
||||
class VideoAttachment extends AttachmentWidget {
|
||||
final MessageTheme messageTheme;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
|
||||
const VideoAttachment({
|
||||
Key key,
|
||||
@required Message message,
|
||||
@required Attachment attachment,
|
||||
Size size,
|
||||
this.messageTheme,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return source.when(
|
||||
local: () {
|
||||
if (attachment.file == null) {
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
return _buildVideoAttachment(
|
||||
context,
|
||||
VideoThumbnailImage(
|
||||
video: attachment.file.path,
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __) => AttachmentError(size: size),
|
||||
),
|
||||
);
|
||||
},
|
||||
network: () {
|
||||
if (attachment.assetUrl == null) {
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
return _buildVideoAttachment(
|
||||
context,
|
||||
VideoThumbnailImage(
|
||||
video: attachment.assetUrl,
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __) => AttachmentError(size: size),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoAttachment(BuildContext context, Widget videoWidget) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
child: Column(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (res != null) onReturnAction(res);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
child: videoWidget,
|
||||
),
|
||||
Center(
|
||||
child: Material(
|
||||
shape: CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Icon(Icons.play_arrow),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AttachmentUploadStateBuilder(
|
||||
message: message,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (attachment.title != null)
|
||||
Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
messageTheme: messageTheme,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
class AttachmentError extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
final Size size;
|
||||
|
||||
const AttachmentError({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
this.size,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.localUri != null) {
|
||||
return Image.file(
|
||||
File(attachment.localUri.path),
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: Container(
|
||||
width: size?.width,
|
||||
height: size?.height ?? 200,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'attachment/attachment.dart';
|
||||
|
||||
class ChannelFileDisplayScreen extends StatefulWidget {
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
@@ -164,6 +166,7 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FileAttachment(
|
||||
message: media.values.toList()[position],
|
||||
attachment: media.keys.toList()[position],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'attachment/attachment.dart';
|
||||
|
||||
class ChannelMediaDisplayScreen extends StatefulWidget {
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
class ICompressVideoService {
|
||||
static final ICompressVideoService instance = ICompressVideoService._();
|
||||
final _lock = Lock();
|
||||
|
||||
ICompressVideoService._();
|
||||
|
||||
Future<MediaInfo> compress(String path) async {
|
||||
return _lock.synchronized(() {
|
||||
return VideoCompress.compressVideo(
|
||||
path,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ICompressVideoService get compressVideoService =>
|
||||
ICompressVideoService.instance;
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:characters/characters.dart';
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:http_parser/http_parser.dart' as http_parser;
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
final _emojis = Emoji.all();
|
||||
|
||||
@@ -23,16 +23,6 @@ extension StringExtension on String {
|
||||
if (characters.length > 3) return false;
|
||||
return characters.every((c) => _emojis.map((e) => e.char).contains(c));
|
||||
}
|
||||
|
||||
/// Returns the mime type from the passed file name.
|
||||
http_parser.MediaType get mimeType {
|
||||
if (this == null) return null;
|
||||
if (toLowerCase().endsWith('heic')) {
|
||||
return http_parser.MediaType.parse('image/heic');
|
||||
} else {
|
||||
return http_parser.MediaType.parse(lookupMimeType(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List extension
|
||||
@@ -43,3 +33,14 @@ extension IterableX<T> on Iterable<T> {
|
||||
yield e;
|
||||
}).skip(1).toList(growable: false);
|
||||
}
|
||||
|
||||
///
|
||||
extension PlatformFileX on PlatformFile {
|
||||
///
|
||||
AttachmentFile get toAttachmentFile => AttachmentFile(
|
||||
path: path,
|
||||
name: name,
|
||||
bytes: bytes,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'media_utils.dart';
|
||||
|
||||
enum FileAttachmentType { local, online }
|
||||
|
||||
class FileAttachment extends StatefulWidget {
|
||||
final Attachment attachment;
|
||||
final Size size;
|
||||
final Widget trailing;
|
||||
final FileAttachmentType attachmentType;
|
||||
final PlatformFile file;
|
||||
|
||||
const FileAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
this.size,
|
||||
this.trailing,
|
||||
this.attachmentType = FileAttachmentType.online,
|
||||
this.file,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FileAttachmentState createState() => _FileAttachmentState();
|
||||
}
|
||||
|
||||
class _FileAttachmentState extends State<FileAttachment> {
|
||||
VideoPlayerController _controller;
|
||||
Future<void> _initializeVideoPlayerFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (MediaUtils.getMimeType(widget.attachment.title)?.type == 'video') {
|
||||
if (widget.attachmentType == FileAttachmentType.online) {
|
||||
_controller = VideoPlayerController.network(
|
||||
widget.attachment.assetUrl,
|
||||
);
|
||||
} else {
|
||||
_controller = VideoPlayerController.file(
|
||||
File.fromRawPath(widget.file.bytes),
|
||||
);
|
||||
}
|
||||
|
||||
_initializeVideoPlayerFuture = _controller.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Container(
|
||||
width: widget.size?.width ?? 100,
|
||||
height: 56.0,
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
child: _getFileTypeImage(),
|
||||
height: 40.0,
|
||||
width: 33.33,
|
||||
margin: EdgeInsets.all(8.0),
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.attachment?.title ?? 'File',
|
||||
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 3.0),
|
||||
Text(
|
||||
'${filesize(widget.attachment.extraData['file_size'])}',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: widget.trailing ??
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.cloudDownload(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
splashRadius: 16,
|
||||
onPressed: () {
|
||||
launchURL(context, widget.attachment.assetUrl);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getFileTypeImage() {
|
||||
if ((MediaUtils.getMimeType(widget.attachment.title)?.type == 'image')) {
|
||||
switch (widget.attachmentType) {
|
||||
case FileAttachmentType.local:
|
||||
return Image.memory(
|
||||
widget.file.bytes,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, obj, trace) {
|
||||
return getFileTypeImage(widget.attachment.extraData['other']);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case FileAttachmentType.online:
|
||||
return CachedNetworkImage(
|
||||
imageUrl: widget.attachment.imageUrl ??
|
||||
widget.attachment.assetUrl ??
|
||||
widget.attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, obj, trace) {
|
||||
return getFileTypeImage(widget.attachment.extraData['other']);
|
||||
},
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((MediaUtils.getMimeType(widget.attachment.title)?.type == 'video')) {
|
||||
switch (widget.attachmentType) {
|
||||
case FileAttachmentType.local:
|
||||
return FutureBuilder<File>(
|
||||
future: VideoCompress.getFileThumbnail(widget.file.path),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Image.asset(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
);
|
||||
}
|
||||
|
||||
return Image.file(
|
||||
snapshot.data,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case FileAttachmentType.online:
|
||||
return FutureBuilder(
|
||||
future: _initializeVideoPlayerFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller),
|
||||
);
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return getFileTypeImage(widget.attachment.extraData['mime_type']);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -48,36 +51,34 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
|
||||
int _currentPage;
|
||||
|
||||
List<VideoPackage> videoPackages = [];
|
||||
final videoPackages = <String, VideoPackage>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller =
|
||||
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 300),
|
||||
);
|
||||
_pageController = PageController(initialPage: widget.startIndex);
|
||||
_currentPage = widget.startIndex;
|
||||
widget.mediaAttachments
|
||||
.where((element) => element.type == 'video')
|
||||
.toList()
|
||||
.forEach((element) {
|
||||
videoPackages.add(VideoPackage(
|
||||
context,
|
||||
element,
|
||||
() {
|
||||
setState(() {});
|
||||
},
|
||||
showControls: true,
|
||||
));
|
||||
});
|
||||
for (final attachment in widget.mediaAttachments) {
|
||||
if (attachment.type != 'video') continue;
|
||||
final package = VideoPackage(attachment, showControls: true);
|
||||
videoPackages[attachment.id] = package;
|
||||
}
|
||||
initializePlayers();
|
||||
}
|
||||
|
||||
Future<void> initializePlayers() async {
|
||||
await Future.wait(videoPackages.values.map(
|
||||
(it) => it.initialize(),
|
||||
));
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var videoAttachments = widget.mediaAttachments
|
||||
.where((element) => element.type == 'video')
|
||||
.toList();
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Stack(
|
||||
@@ -92,14 +93,18 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
_currentPage = val;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, position) {
|
||||
if (widget.mediaAttachments[position].type == 'image' ||
|
||||
widget.mediaAttachments[position].type == 'giphy') {
|
||||
itemBuilder: (context, index) {
|
||||
final attachment = widget.mediaAttachments[index];
|
||||
if (attachment.type == 'image' ||
|
||||
attachment.type == 'giphy') {
|
||||
final imageUrl = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
return PhotoView(
|
||||
imageProvider: CachedNetworkImageProvider(
|
||||
widget.mediaAttachments[position].imageUrl ??
|
||||
widget.mediaAttachments[position].assetUrl ??
|
||||
widget.mediaAttachments[position].thumbUrl),
|
||||
imageProvider:
|
||||
imageUrl == null && attachment.localUri != null
|
||||
? Image.memory(attachment.file.bytes).image
|
||||
: CachedNetworkImageProvider(imageUrl),
|
||||
maxScale: PhotoViewComputedScale.covered,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
@@ -125,12 +130,9 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if (widget.mediaAttachments[position].type ==
|
||||
'video') {
|
||||
var controllerPackage = videoPackages[videoAttachments
|
||||
.indexOf(widget.mediaAttachments[position])];
|
||||
|
||||
if (!controllerPackage.initialised) {
|
||||
} else if (attachment.type == 'video') {
|
||||
final controller = videoPackages[attachment.id];
|
||||
if (!controller.initialized) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
@@ -151,7 +153,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
vertical: 50.0,
|
||||
),
|
||||
child: Chewie(
|
||||
controller: controllerPackage.chewieController,
|
||||
controller: controller.chewieController,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -188,7 +190,6 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
totalPages: widget.mediaAttachments.length,
|
||||
mediaAttachments: widget.mediaAttachments,
|
||||
message: widget.message,
|
||||
videoPackages: videoPackages,
|
||||
mediaSelectedCallBack: (val) {
|
||||
setState(() {
|
||||
_currentPage = val;
|
||||
@@ -224,54 +225,58 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
videoPackages.forEach((element) {
|
||||
element.dispose();
|
||||
});
|
||||
void dispose() async {
|
||||
for (final package in videoPackages.values) {
|
||||
await package.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class VideoPackage {
|
||||
VideoPlayerController _videoPlayerController;
|
||||
final bool _showControls;
|
||||
final bool _autoInitialize;
|
||||
final VideoPlayerController _videoPlayerController;
|
||||
ChewieController _chewieController;
|
||||
bool initialised = false;
|
||||
VoidCallback onInit;
|
||||
BuildContext context;
|
||||
bool showControls;
|
||||
|
||||
///
|
||||
VideoPackage(this.context, Attachment attachment, this.onInit,
|
||||
{this.showControls = false}) {
|
||||
_videoPlayerController = VideoPlayerController.network(attachment.assetUrl);
|
||||
_videoPlayerController.initialize().whenComplete(() {
|
||||
initialised = true;
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController,
|
||||
autoInitialize: true,
|
||||
showControls: showControls,
|
||||
aspectRatio: _videoPlayerController.value.aspectRatio,
|
||||
);
|
||||
onInit();
|
||||
});
|
||||
|
||||
VoidCallback errorListener;
|
||||
errorListener = () {
|
||||
if (_videoPlayerController.value.hasError) {
|
||||
Navigator.pop(context);
|
||||
launchURL(context, attachment.titleLink);
|
||||
}
|
||||
_videoPlayerController.removeListener(errorListener);
|
||||
};
|
||||
_videoPlayerController.addListener(errorListener);
|
||||
}
|
||||
|
||||
VideoPlayerController get videoPlayer => _videoPlayerController;
|
||||
|
||||
ChewieController get chewieController => _chewieController;
|
||||
|
||||
void dispose() {
|
||||
_videoPlayerController.dispose();
|
||||
_chewieController.dispose();
|
||||
bool get initialized => _videoPlayerController.value.initialized;
|
||||
|
||||
VideoPackage(
|
||||
Attachment attachment, {
|
||||
bool showControls = false,
|
||||
bool autoInitialize = true,
|
||||
}) : assert(attachment != null),
|
||||
_showControls = showControls,
|
||||
_autoInitialize = autoInitialize,
|
||||
_videoPlayerController = attachment.localUri != null
|
||||
? VideoPlayerController.file(File.fromUri(attachment.localUri))
|
||||
: VideoPlayerController.network(attachment.assetUrl);
|
||||
|
||||
Future<void> initialize() {
|
||||
return _videoPlayerController.initialize().then((_) {
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController,
|
||||
autoInitialize: _autoInitialize,
|
||||
showControls: _showControls,
|
||||
aspectRatio: _videoPlayerController.value.aspectRatio,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void addListener(VoidCallback listener) {
|
||||
return _videoPlayerController.addListener(listener);
|
||||
}
|
||||
|
||||
void removeListener(VoidCallback listener) {
|
||||
return _videoPlayerController.removeListener(listener);
|
||||
}
|
||||
|
||||
Future<void> dispose() {
|
||||
_chewieController?.dispose();
|
||||
return _videoPlayerController?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +105,9 @@ class ImageActionsModal extends StatelessWidget {
|
||||
() {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
StreamChat.of(context).client.deleteMessage(
|
||||
message,
|
||||
StreamChannel.of(context).channel.cid,
|
||||
);
|
||||
StreamChannel.of(context)
|
||||
.channel
|
||||
.deleteMessage(message);
|
||||
},
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'attachment_error.dart';
|
||||
import 'attachment_title.dart';
|
||||
import 'full_screen_media.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
class ImageAttachment extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
final Message message;
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final bool showTitle;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
|
||||
const ImageAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
@required this.message,
|
||||
@required this.size,
|
||||
this.messageTheme,
|
||||
this.showTitle = true,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.thumbUrl == null &&
|
||||
attachment.imageUrl == null &&
|
||||
attachment.assetUrl == null) {
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
);
|
||||
}
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
var result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [
|
||||
attachment,
|
||||
],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
onReturnAction(result);
|
||||
}
|
||||
},
|
||||
child: CachedNetworkImage(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) {
|
||||
return Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: attachment.thumbUrl ??
|
||||
attachment.imageUrl ??
|
||||
attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showTitle && attachment.title != null)
|
||||
Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
messageTheme: messageTheme,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showTitle &&
|
||||
(attachment.titleLink != null || attachment.ogScrapeUrl != null))
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => launchURL(
|
||||
context,
|
||||
attachment.titleLink ?? attachment.ogScrapeUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:chewie/chewie.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: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';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
@@ -28,7 +28,6 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
final List<Attachment> mediaAttachments;
|
||||
final Message message;
|
||||
|
||||
final List<VideoPackage> videoPackages;
|
||||
final ValueChanged<int> mediaSelectedCallBack;
|
||||
|
||||
/// Creates a channel header
|
||||
@@ -41,7 +40,6 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
this.totalPages = 0,
|
||||
this.mediaAttachments,
|
||||
this.message,
|
||||
this.videoPackages,
|
||||
this.mediaSelectedCallBack,
|
||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
@@ -214,17 +212,14 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
itemBuilder: (context, index) {
|
||||
Widget media;
|
||||
final attachment = widget.mediaAttachments[index];
|
||||
|
||||
if (attachment.type == 'video') {
|
||||
var controllerPackage = widget.videoPackages[
|
||||
videoAttachments.indexOf(attachment)];
|
||||
|
||||
media = InkWell(
|
||||
onTap: () => widget.mediaSelectedCallBack(index),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: Chewie(
|
||||
controller: controllerPackage.chewieController,
|
||||
child: VideoThumbnailImage(
|
||||
video: attachment.file?.path ??
|
||||
attachment.assetUrl,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||
@@ -9,12 +8,14 @@ class ImageGroup extends StatelessWidget {
|
||||
Key key,
|
||||
@required this.images,
|
||||
@required this.message,
|
||||
@required this.messageTheme,
|
||||
@required this.size,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<Attachment> images;
|
||||
final Message message;
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
@@ -129,14 +130,12 @@ class ImageGroup extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildImage(BuildContext context, int index) {
|
||||
return GestureDetector(
|
||||
onTap: () => _onTap(context, index),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: images[index].imageUrl ??
|
||||
images[index].thumbUrl ??
|
||||
images[index].assetUrl,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
return ImageAttachment(
|
||||
attachment: images[index],
|
||||
size: size,
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
onAttachmentTap: () => _onTap(context, index),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:http_parser/http_parser.dart' as http_parser;
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
class MediaUtils {
|
||||
static http_parser.MediaType getMimeType(String filename) {
|
||||
http_parser.MediaType mimeType;
|
||||
if (filename != null) {
|
||||
if (filename.toLowerCase().endsWith('heic')) {
|
||||
mimeType = http_parser.MediaType.parse('image/heic');
|
||||
} else {
|
||||
mimeType = http_parser.MediaType.parse(lookupMimeType(filename));
|
||||
}
|
||||
}
|
||||
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ class MessageActionsModal extends StatefulWidget {
|
||||
final ShapeBorder messageShape;
|
||||
final ShapeBorder attachmentShape;
|
||||
final DisplayWidget showUserAvatar;
|
||||
final Map<String, VideoPackage> videoPackages;
|
||||
|
||||
const MessageActionsModal({
|
||||
Key key,
|
||||
@@ -54,7 +53,6 @@ class MessageActionsModal extends StatefulWidget {
|
||||
this.messageShape,
|
||||
this.attachmentShape,
|
||||
this.reverse = false,
|
||||
this.videoPackages,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -182,7 +180,6 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
showSendingIndicator: false,
|
||||
shape: widget.messageShape,
|
||||
attachmentShape: widget.attachmentShape,
|
||||
videoPackages: widget.videoPackages,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
@@ -298,10 +295,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
if (answer) {
|
||||
try {
|
||||
Navigator.pop(context);
|
||||
await StreamChat.of(context).client.deleteMessage(
|
||||
widget.message,
|
||||
StreamChannel.of(context).channel.cid,
|
||||
);
|
||||
await StreamChannel.of(context).channel.deleteMessage(widget.message);
|
||||
} catch (err) {
|
||||
_showErrorAlert();
|
||||
}
|
||||
@@ -570,10 +564,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
final client = StreamChat.of(context).client;
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (isUpdateFailed) {
|
||||
client.updateMessage(widget.message, channel.cid);
|
||||
channel.updateMessage(widget.message);
|
||||
} else {
|
||||
channel.sendMessage(widget.message);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -11,7 +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:stream_chat_flutter/src/compress_video_service.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';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
@@ -19,16 +19,16 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:substring_highlight/substring_highlight.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'attachment_uploader.dart';
|
||||
import 'attachment/attachment.dart';
|
||||
import 'extension.dart';
|
||||
import 'quoted_message_widget.dart';
|
||||
import 'video_thumbnail_image.dart';
|
||||
|
||||
typedef AttachmentThumbnailBuilder = Widget Function(
|
||||
BuildContext,
|
||||
_SendingAttachment,
|
||||
Attachment,
|
||||
);
|
||||
|
||||
enum ActionsLocation {
|
||||
@@ -44,7 +44,7 @@ enum DefaultAttachmentTypes {
|
||||
|
||||
const _kMinMediaPickerSize = 360.0;
|
||||
|
||||
const _kMaxAttachmentSize = 20480; //20MB
|
||||
const _kMaxAttachmentSize = 20971520; // 20MB in Bytes
|
||||
|
||||
/// Inactive state
|
||||
/// 
|
||||
@@ -99,7 +99,6 @@ class MessageInput extends StatefulWidget {
|
||||
this.maxHeight = 150,
|
||||
this.keyboardType = TextInputType.multiline,
|
||||
this.disableAttachments = false,
|
||||
this.attachmentUploader,
|
||||
this.initialMessage,
|
||||
this.textEditingController,
|
||||
this.actions,
|
||||
@@ -135,9 +134,6 @@ class MessageInput extends StatefulWidget {
|
||||
/// If true the attachments button will not be displayed
|
||||
final bool disableAttachments;
|
||||
|
||||
/// A delegate to upload attachments
|
||||
final AttachmentUploader attachmentUploader;
|
||||
|
||||
/// The text controller of the TextField
|
||||
final TextEditingController textEditingController;
|
||||
|
||||
@@ -178,7 +174,7 @@ class MessageInput extends StatefulWidget {
|
||||
}
|
||||
|
||||
class MessageInputState extends State<MessageInput> {
|
||||
final _attachments = <String, _SendingAttachment>{};
|
||||
final _attachments = <String, Attachment>{};
|
||||
final List<User> _mentionedUsers = [];
|
||||
|
||||
final _imagePicker = ImagePicker();
|
||||
@@ -204,8 +200,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||
|
||||
AttachmentUploader _attachmentUploader;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -391,8 +385,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AnimatedCrossFade(
|
||||
crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) &&
|
||||
_attachments.values.every((a) => a.isUploaded == true))
|
||||
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: _buildSendButton(context),
|
||||
@@ -786,7 +779,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
Widget _buildFilePickerSection() {
|
||||
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||
return it.attachmentType == 'file';
|
||||
return it.type == 'file';
|
||||
});
|
||||
|
||||
Color _getIconColor(int index) {
|
||||
@@ -943,7 +936,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
Widget _buildPickerSection() {
|
||||
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||
return it.attachmentType == 'file';
|
||||
return it.type == 'file';
|
||||
});
|
||||
|
||||
switch (_filePickerIndex) {
|
||||
@@ -1045,98 +1038,47 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
void _addAttachment(AssetEntity medium) async {
|
||||
final attachmentId = medium.id;
|
||||
_attachments[attachmentId] = _SendingAttachment(id: attachmentId);
|
||||
try {
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
Duration(seconds: 5),
|
||||
onTimeout: () => medium.originFile,
|
||||
);
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
Duration(seconds: 5),
|
||||
onTimeout: () => medium.originFile,
|
||||
);
|
||||
|
||||
var file = PlatformFile(
|
||||
path: mediaFile.path,
|
||||
size: ((await mediaFile.length()) / 1024).ceil(),
|
||||
bytes: mediaFile.readAsBytesSync(),
|
||||
);
|
||||
var file = AttachmentFile(
|
||||
path: mediaFile.path,
|
||||
size: await mediaFile.length(),
|
||||
bytes: mediaFile.readAsBytesSync(),
|
||||
);
|
||||
|
||||
if (file.size > _kMaxAttachmentSize) {
|
||||
if (medium?.type == AssetType.video) {
|
||||
final mediaInfo = await compressVideoService.compress(file.path);
|
||||
if (file.size > _kMaxAttachmentSize) {
|
||||
if (medium?.type == AssetType.video) {
|
||||
final mediaInfo = await VideoService.compressVideo(file.path);
|
||||
|
||||
if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) {
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
|
||||
);
|
||||
_attachments.remove(attachmentId);
|
||||
return;
|
||||
}
|
||||
file = PlatformFile(
|
||||
name: file.name,
|
||||
size: (mediaInfo.filesize / 1024).ceil(),
|
||||
bytes: await mediaInfo.file.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
if (mediaInfo.filesize > _kMaxAttachmentSize) {
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB.',
|
||||
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(
|
||||
file: file,
|
||||
attachment: Attachment(
|
||||
localUri: file.path != null ? Uri.parse(file.path) : null,
|
||||
type: medium?.type == AssetType.image ? 'image' : 'video',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
final fileType = medium.type == AssetType.image
|
||||
? DefaultAttachmentTypes.image
|
||||
: DefaultAttachmentTypes.video;
|
||||
|
||||
final url = await _uploadAttachment(
|
||||
file,
|
||||
fileType,
|
||||
onSendProgress: (sent, total) {
|
||||
setState(() {
|
||||
_attachments.update(
|
||||
attachmentId,
|
||||
(it) => it.copyWith(totalUploaded: sent, totalSize: total),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (fileType == DefaultAttachmentTypes.image) {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(attachment: it.attachment.copyWith(imageUrl: url));
|
||||
});
|
||||
file = AttachmentFile(
|
||||
name: file.name,
|
||||
size: mediaInfo.filesize,
|
||||
bytes: await mediaInfo.file.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(attachment: it.attachment.copyWith(assetUrl: url));
|
||||
});
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB.',
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
// Marking as upload complete
|
||||
_attachments.update(
|
||||
attachmentId,
|
||||
(it) => it.copyWith(totalUploaded: it.totalSize),
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (e, s) {
|
||||
setState(() => _attachments.remove(attachmentId));
|
||||
print(e);
|
||||
print(s);
|
||||
_showErrorAlert('Error adding the attachment: $e');
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_attachments[medium.id] = Attachment(
|
||||
id: medium.id,
|
||||
file: file,
|
||||
type: medium.type == AssetType.image ? 'image' : 'video',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildCommandIcon(String iconType) {
|
||||
@@ -1561,10 +1503,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
Widget _buildAttachments() {
|
||||
if (_attachments.isEmpty) return Offstage();
|
||||
final fileAttachments = _attachments.values
|
||||
.where((it) => it.attachmentType == 'file')
|
||||
.where((it) => it.type == 'file')
|
||||
.toList(growable: false);
|
||||
final remainingAttachments = _attachments.values
|
||||
.where((it) => it.attachmentType != 'file')
|
||||
.where((it) => it.type != 'file')
|
||||
.toList(growable: false);
|
||||
return Column(
|
||||
children: [
|
||||
@@ -1582,37 +1524,20 @@ class MessageInputState extends State<MessageInput> {
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: FileAttachment(
|
||||
attachment: e.attachment,
|
||||
attachmentType: FileAttachmentType.local,
|
||||
file: e.file,
|
||||
message: null,
|
||||
attachment: e,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.65,
|
||||
56.0,
|
||||
),
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: InkWell(
|
||||
child: CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.6),
|
||||
maxRadius: 12.0,
|
||||
child: StreamSvgIcon.close(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => _attachments.remove(e.id));
|
||||
},
|
||||
),
|
||||
child: _buildRemoveButton(e),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.insertBetween(const SizedBox(width: 8)),
|
||||
.insertBetween(const SizedBox(height: 8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1638,16 +1563,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: _buildAttachment(attachment),
|
||||
),
|
||||
),
|
||||
_buildRemoveButton(attachment),
|
||||
if (!attachment.isUploaded)
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: _buildRemoveButton(attachment),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1660,44 +1580,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadProgressIndicator(int uploaded, int total) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
Widget _buildRemoveButton(Attachment attachment) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorTheme.overlayDark.withOpacity(0.6),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 5, bottom: 5, right: 11, left: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(Color(0xffb2b2b2)),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'${filesize(uploaded)} / ${filesize(total)}',
|
||||
style: theme.textTheme.footnote.copyWith(
|
||||
color: theme.colorTheme.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Positioned _buildRemoveButton(_SendingAttachment attachment) {
|
||||
return Positioned(
|
||||
height: 24,
|
||||
width: 24,
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -1721,21 +1607,18 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachment(_SendingAttachment attachment) {
|
||||
if (widget.attachmentThumbnailBuilders
|
||||
?.containsKey(attachment.attachmentType) ==
|
||||
Widget _buildAttachment(Attachment attachment) {
|
||||
if (attachment == null) return Offstage();
|
||||
|
||||
if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) ==
|
||||
true) {
|
||||
return widget.attachmentThumbnailBuilders[attachment.attachmentType](
|
||||
return widget.attachmentThumbnailBuilders[attachment.type](
|
||||
context,
|
||||
attachment,
|
||||
);
|
||||
}
|
||||
|
||||
if (attachment.attachment == null) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
switch (attachment.attachmentType) {
|
||||
switch (attachment.type) {
|
||||
case 'image':
|
||||
case 'giphy':
|
||||
return attachment.file != null
|
||||
@@ -1749,32 +1632,33 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
},
|
||||
)
|
||||
: Image.network(
|
||||
attachment.attachment.imageUrl,
|
||||
: CachedNetworkImage(
|
||||
imageUrl: attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, obj, trace) {
|
||||
return getFileTypeImage(attachment.extraData['other']);
|
||||
},
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'video':
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
child: FutureBuilder<File>(
|
||||
future: VideoCompress.getFileThumbnail(attachment.file.path),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Image.asset(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
);
|
||||
}
|
||||
|
||||
return Image.file(
|
||||
snapshot.data,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
height: 104,
|
||||
width: 104,
|
||||
child: VideoThumbnailImage(
|
||||
video: attachment.file.path,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -1787,7 +1671,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
],
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return Container(
|
||||
child: Icon(Icons.insert_drive_file),
|
||||
@@ -1953,11 +1836,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
/// Use this to add custom type attachments
|
||||
void addAttachment(Attachment attachment) {
|
||||
setState(() {
|
||||
final _attachment = _SendingAttachment(
|
||||
attachment: attachment,
|
||||
totalUploaded: attachment.extraData['file_size'],
|
||||
_attachments[attachment.id] = attachment.copyWith(
|
||||
uploadState: attachment.uploadState ?? UploadState.success(),
|
||||
);
|
||||
_attachments[_attachment.id] = _attachment;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1966,7 +1847,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async {
|
||||
setState(() => _inputEnabled = false);
|
||||
|
||||
PlatformFile file;
|
||||
AttachmentFile file;
|
||||
String attachmentType;
|
||||
|
||||
if (fileType == DefaultAttachmentTypes.image) {
|
||||
@@ -1988,8 +1869,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
return;
|
||||
}
|
||||
final bytes = await pickedFile.readAsBytes();
|
||||
file = PlatformFile(
|
||||
size: (bytes.length / 1024).ceil(),
|
||||
file = AttachmentFile(
|
||||
size: bytes.length,
|
||||
path: pickedFile.path,
|
||||
bytes: bytes,
|
||||
);
|
||||
@@ -2007,7 +1888,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
withData: true,
|
||||
);
|
||||
if (res?.files?.isNotEmpty == true) {
|
||||
file = res.files.single;
|
||||
file = res.files.single.toAttachmentFile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2017,7 +1898,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
final mimeType = file.path.split('/').last.mimeType;
|
||||
|
||||
var extraDataMap = <String, dynamic>{};
|
||||
final extraDataMap = <String, dynamic>{};
|
||||
|
||||
if (camera) {
|
||||
if (mimeType.type == 'video' || mimeType.type == 'image') {
|
||||
@@ -2035,97 +1916,46 @@ class MessageInputState extends State<MessageInput> {
|
||||
extraDataMap['file_size'] = file.size;
|
||||
}
|
||||
|
||||
final attachment = _SendingAttachment(
|
||||
final attachment = Attachment(
|
||||
file: file,
|
||||
attachment: Attachment(
|
||||
localUri: file.path != null ? Uri.parse(file.path) : null,
|
||||
type: attachmentType,
|
||||
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
|
||||
title: file.name,
|
||||
),
|
||||
type: attachmentType,
|
||||
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
|
||||
);
|
||||
final attachmentId = attachment.id;
|
||||
|
||||
setState(() => _attachments[attachmentId] = attachment);
|
||||
_attachments[attachment.id] = attachment;
|
||||
|
||||
if (file.size / 1024 > _kMaxAttachmentSize) {
|
||||
if (attachmentType == 'video') {
|
||||
final mediaInfo = await compressVideoService.compress(file.path);
|
||||
file = PlatformFile(
|
||||
name: mediaInfo.title,
|
||||
size: (mediaInfo.filesize / 1024).ceil(),
|
||||
if (file.size > _kMaxAttachmentSize) {
|
||||
if (attachmentType == 'Video') {
|
||||
final mediaInfo = await VideoService.compressVideo(file.path);
|
||||
|
||||
if (mediaInfo.filesize > _kMaxAttachmentSize) {
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
|
||||
);
|
||||
_attachments.remove(attachment.id);
|
||||
return;
|
||||
}
|
||||
file = AttachmentFile(
|
||||
name: file.name,
|
||||
size: mediaInfo.filesize,
|
||||
bytes: await mediaInfo.file.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
setState(() {
|
||||
_attachments.update(attachmentId, (it) => it.copyWith(file: file));
|
||||
});
|
||||
} else {
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB.',
|
||||
);
|
||||
setState(() => _attachments.remove(attachmentId));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final url = await _uploadAttachment(
|
||||
file,
|
||||
fileType,
|
||||
onSendProgress: (sent, total) {
|
||||
setState(() {
|
||||
_attachments.update(
|
||||
attachmentId,
|
||||
(it) => it.copyWith(totalUploaded: sent, totalSize: total),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (fileType == DefaultAttachmentTypes.image) {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(attachment: it.attachment.copyWith(imageUrl: url));
|
||||
});
|
||||
} else {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(attachment: it.attachment.copyWith(assetUrl: url));
|
||||
});
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
// Marking as upload complete
|
||||
_attachments.update(
|
||||
attachmentId,
|
||||
(it) => it.copyWith(totalUploaded: it.totalSize),
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (e, s) {
|
||||
setState(() => _attachments.remove(attachmentId));
|
||||
print(e);
|
||||
print(s);
|
||||
_showErrorAlert('Error adding the attachment: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _uploadAttachment(
|
||||
PlatformFile file,
|
||||
DefaultAttachmentTypes type, {
|
||||
ProgressCallback onSendProgress,
|
||||
}) {
|
||||
if (type == DefaultAttachmentTypes.image) {
|
||||
return _attachmentUploader.uploadImage(
|
||||
file,
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
} else {
|
||||
return _attachmentUploader.uploadFile(
|
||||
file,
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
_attachments.update(attachment.id, (it) {
|
||||
return it.copyWith(
|
||||
file: file,
|
||||
extraData: {...it.extraData}..update('file_size', (_) => file.size),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildIdleSendButton(BuildContext context) {
|
||||
@@ -2203,7 +2033,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (widget.editMessage != null) {
|
||||
message = widget.editMessage.copyWith(
|
||||
text: text,
|
||||
attachments: _getAttachments(attachments).toList(),
|
||||
attachments: attachments,
|
||||
mentionedUsers:
|
||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
||||
);
|
||||
@@ -2211,7 +2041,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
message = (widget.initialMessage ?? Message()).copyWith(
|
||||
parentId: widget.parentMessage?.id,
|
||||
text: text,
|
||||
attachments: _getAttachments(attachments).toList(),
|
||||
attachments: attachments,
|
||||
mentionedUsers:
|
||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
||||
showInChannel: widget.parentMessage != null ? _sendAsDm : null,
|
||||
@@ -2237,13 +2067,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
_mentionedUsers.clear();
|
||||
|
||||
if (widget.editMessage == null ||
|
||||
widget.editMessage.status == MessageSendingStatus.failed) {
|
||||
widget.editMessage.status == MessageSendingStatus.failed ||
|
||||
widget.editMessage.status == MessageSendingStatus.sending) {
|
||||
sendingFuture = channel.sendMessage(message);
|
||||
} else {
|
||||
sendingFuture = StreamChat.of(context).client.updateMessage(
|
||||
message,
|
||||
channel.cid,
|
||||
);
|
||||
sendingFuture = channel.updateMessage(message);
|
||||
}
|
||||
|
||||
return sendingFuture.then((resp) {
|
||||
@@ -2253,12 +2081,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
});
|
||||
}
|
||||
|
||||
Iterable<Attachment> _getAttachments(List<_SendingAttachment> attachments) {
|
||||
return attachments.map((attachment) {
|
||||
return attachment.attachment;
|
||||
});
|
||||
}
|
||||
|
||||
StreamSubscription _keyboardListener;
|
||||
|
||||
void _showErrorAlert(String description) {
|
||||
@@ -2334,16 +2156,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
void _parseExistingMessage(Message message) {
|
||||
textEditingController.text = message.text;
|
||||
|
||||
_messageIsPresent = true;
|
||||
|
||||
message.attachments?.forEach((attachment) {
|
||||
final _attachment = _SendingAttachment(
|
||||
attachment: attachment,
|
||||
totalUploaded: attachment.extraData['file_size'],
|
||||
for (final attachment in message.attachments) {
|
||||
_attachments[attachment.id] = attachment.copyWith(
|
||||
uploadState: attachment.uploadState ?? UploadState.success(),
|
||||
);
|
||||
_attachments[_attachment.id] = _attachment;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2363,63 +2181,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
_initialized = true;
|
||||
}
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (_attachmentUploader == null) {
|
||||
_attachmentUploader =
|
||||
widget.attachmentUploader ?? StreamAttachmentUploader(channel);
|
||||
} else if (_attachmentUploader is StreamAttachmentUploader) {
|
||||
_attachmentUploader = StreamAttachmentUploader(channel);
|
||||
}
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
}
|
||||
|
||||
class _SendingAttachment {
|
||||
_SendingAttachment({
|
||||
String id,
|
||||
this.file,
|
||||
this.attachment,
|
||||
this.totalUploaded = 0,
|
||||
int totalSize,
|
||||
}) : id = id ?? shortHash(DateTime.now().millisecondsSinceEpoch),
|
||||
attachmentType = attachment?.type,
|
||||
totalSize =
|
||||
totalSize ?? file?.size ?? attachment.extraData['file_size'];
|
||||
|
||||
final String id;
|
||||
final PlatformFile file;
|
||||
final Attachment attachment;
|
||||
final String attachmentType;
|
||||
|
||||
final int totalUploaded;
|
||||
final int totalSize;
|
||||
|
||||
// Progress while the attachment is uploading to the server
|
||||
// 0 -> 100
|
||||
double get uploadPercentage {
|
||||
if (totalSize == null) return null;
|
||||
return (totalUploaded / totalSize) * 100;
|
||||
}
|
||||
|
||||
bool get isUploaded => uploadPercentage == 100;
|
||||
|
||||
_SendingAttachment copyWith({
|
||||
String id,
|
||||
PlatformFile file,
|
||||
Attachment attachment,
|
||||
int totalUploaded,
|
||||
int totalSize,
|
||||
}) {
|
||||
return _SendingAttachment(
|
||||
id: id ?? this.id,
|
||||
file: file ?? this.file,
|
||||
attachment: attachment ?? this.attachment,
|
||||
totalUploaded: totalUploaded ?? this.totalUploaded,
|
||||
totalSize: totalSize ?? this.totalSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a 2-tuple, or pair.
|
||||
class Tuple2<T1, T2> {
|
||||
/// Returns the first item of the tuple
|
||||
|
||||
@@ -239,46 +239,50 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
final MessageListController _messageListController = MessageListController();
|
||||
|
||||
final Map<String, VideoPackage> videoPackages = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MessageListCore(
|
||||
loadingBuilder: (context) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
emptyBuilder: (context) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
},
|
||||
messageListBuilder: (context, list) {
|
||||
return _buildListView(list);
|
||||
},
|
||||
messageListController: _messageListController,
|
||||
parentMessage: widget.parentMessage,
|
||||
showScrollToBottom: widget.showScrollToBottom,
|
||||
errorWidgetBuilder: (BuildContext context, Object error) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Something went wrong',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
print('Getting popped');
|
||||
return false;
|
||||
},
|
||||
child: MessageListCore(
|
||||
loadingBuilder: (context) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
emptyBuilder: (context) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
},
|
||||
messageListBuilder: (context, list) {
|
||||
return _buildListView(list);
|
||||
},
|
||||
messageListController: _messageListController,
|
||||
parentMessage: widget.parentMessage,
|
||||
showScrollToBottom: widget.showScrollToBottom,
|
||||
errorWidgetBuilder: (BuildContext context, Object error) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Something went wrong',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -778,7 +782,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
break;
|
||||
}
|
||||
},
|
||||
videoPackages: videoPackages,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -939,10 +942,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
break;
|
||||
}
|
||||
},
|
||||
videoPackages: videoPackages,
|
||||
);
|
||||
|
||||
if (!message.isDeleted && !message.isSystem && !message.isEphemeral) {
|
||||
if (!message.isDeleted &&
|
||||
!message.isSystem &&
|
||||
!message.isEphemeral &&
|
||||
widget.onMessageSwiped != null) {
|
||||
child = Swipeable(
|
||||
onSwipeEnd: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
@@ -1056,7 +1061,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
streamChannel.reloadChannel();
|
||||
}
|
||||
_messageNewListener?.cancel();
|
||||
videoPackages.values.forEach((e) => e.dispose());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
final ShapeBorder messageShape;
|
||||
final ShapeBorder attachmentShape;
|
||||
final void Function(User) onUserAvatarTap;
|
||||
final Map<String, VideoPackage> videoPackages;
|
||||
|
||||
const MessageReactionsModal({
|
||||
Key key,
|
||||
@@ -37,7 +36,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
this.reverse = false,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.onUserAvatarTap,
|
||||
this.videoPackages,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -146,7 +144,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
(message.status ==
|
||||
MessageSendingStatus.sent ||
|
||||
message.status == null),
|
||||
videoPackages: videoPackages,
|
||||
),
|
||||
),
|
||||
if (message.latestReactions?.isNotEmpty == true) ...[
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||
import 'package:stream_chat_flutter/src/url_attachment.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'attachment/attachment.dart';
|
||||
import 'extension.dart';
|
||||
import 'image_group.dart';
|
||||
import 'message_text.dart';
|
||||
@@ -142,9 +143,6 @@ class MessageWidget extends StatefulWidget {
|
||||
/// Function called when quotedMessage is tapped
|
||||
final OnQuotedMessageTap onQuotedMessageTap;
|
||||
|
||||
/// The cache for the video controllers of attachments IDed as message ID + attachment index
|
||||
final Map<String, VideoPackage> videoPackages;
|
||||
|
||||
///
|
||||
MessageWidget({
|
||||
Key key,
|
||||
@@ -193,7 +191,6 @@ class MessageWidget extends StatefulWidget {
|
||||
this.attachmentPadding = EdgeInsets.zero,
|
||||
this.allRead = false,
|
||||
this.onQuotedMessageTap,
|
||||
this.videoPackages,
|
||||
}) : attachmentBuilders = {
|
||||
'image': (context, message, attachment) {
|
||||
return ImageAttachment(
|
||||
@@ -236,6 +233,7 @@ class MessageWidget extends StatefulWidget {
|
||||
},
|
||||
'file': (context, message, attachment) {
|
||||
return FileAttachment(
|
||||
message: message,
|
||||
attachment: attachment,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
@@ -250,7 +248,8 @@ class MessageWidget extends StatefulWidget {
|
||||
_MessageWidgetState createState() => _MessageWidgetState();
|
||||
}
|
||||
|
||||
class _MessageWidgetState extends State<MessageWidget> {
|
||||
class _MessageWidgetState extends State<MessageWidget>
|
||||
with AutomaticKeepAliveClientMixin<MessageWidget> {
|
||||
bool get showThreadReplyIndicator => widget.showThreadReplyIndicator;
|
||||
|
||||
bool get showSendingIndicator => widget.showSendingIndicator;
|
||||
@@ -298,8 +297,12 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
showSendingIndicator ||
|
||||
isDeleted;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => widget.message.attachments?.isNotEmpty == true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final avatarWidth = widget.messageTheme.avatarTheme.constraints.maxWidth;
|
||||
var leftPadding =
|
||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
||||
@@ -744,7 +747,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
!isFailedState &&
|
||||
widget.onThreadTap != null,
|
||||
showFlagButton: widget.showFlagButton,
|
||||
videoPackages: widget.videoPackages,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -773,7 +775,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
onThreadTap: widget.onThreadTap,
|
||||
showReactions: widget.showReactions,
|
||||
videoPackages: widget.videoPackages,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -824,6 +825,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
),
|
||||
images: images,
|
||||
message: widget.message,
|
||||
messageTheme: widget.messageTheme,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
),
|
||||
),
|
||||
@@ -838,41 +840,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
children: widget.message.attachments
|
||||
?.where((element) => element.ogScrapeUrl == null)
|
||||
?.map((attachment) {
|
||||
if (attachment.type == 'video') {
|
||||
VideoPackage package;
|
||||
|
||||
if (widget.videoPackages == null) {
|
||||
package = VideoPackage(context, attachment, () {});
|
||||
} else {
|
||||
package = widget?.videoPackages[
|
||||
'${widget.message.id}${widget.message.attachments.indexOf(attachment)}'] ??
|
||||
VideoPackage(context, attachment, () {});
|
||||
}
|
||||
|
||||
if (widget.videoPackages != null) {
|
||||
widget.videoPackages[
|
||||
'${widget.message.id}${widget.message.attachments.indexOf(attachment)}'] =
|
||||
package;
|
||||
}
|
||||
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: VideoAttachment(
|
||||
attachment: attachment,
|
||||
messageTheme: widget.messageTheme,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
MediaQuery.of(context).size.height * 0.3,
|
||||
),
|
||||
message: widget.message,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
onReturnAction: widget.onReturnAction,
|
||||
videoPackage: package,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final attachmentBuilder =
|
||||
widget.attachmentBuilders[attachment.type];
|
||||
|
||||
@@ -885,7 +852,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
return wrapAttachmentWidget(
|
||||
context,
|
||||
attachmentWidget,
|
||||
attachment: attachment,
|
||||
);
|
||||
})?.insertBetween(SizedBox(
|
||||
height: widget.attachmentPadding.vertical / 2,
|
||||
@@ -897,9 +863,8 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
|
||||
Widget wrapAttachmentWidget(
|
||||
BuildContext context,
|
||||
Widget attachmentWidget, {
|
||||
Attachment attachment,
|
||||
}) {
|
||||
Widget attachmentWidget,
|
||||
) {
|
||||
final attachmentShape =
|
||||
widget.attachmentShape ?? _getDefaultAttachmentShape(context);
|
||||
return Material(
|
||||
@@ -930,8 +895,29 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
|
||||
Widget _buildSendingIndicator() {
|
||||
final style = widget.messageTheme.createdAt;
|
||||
final message = widget.message;
|
||||
|
||||
if (hasNonUrlAttachments &&
|
||||
(message.status == MessageSendingStatus.sending ||
|
||||
message.status == MessageSendingStatus.updating)) {
|
||||
final totalAttachments = message.attachments.length;
|
||||
final uploadRemaining = message.attachments.where((it) {
|
||||
return !it.uploadState.isSuccess;
|
||||
}).length;
|
||||
if (uploadRemaining == 0) {
|
||||
return StreamSvgIcon.check(
|
||||
size: style.fontSize,
|
||||
color: IconTheme.of(context).color.withOpacity(0.5),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
'Uploading $uploadRemaining/$totalAttachments ...',
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
Widget child = SendingIndicator(
|
||||
message: widget.message,
|
||||
message: message,
|
||||
isMessageRead: isMessageRead,
|
||||
size: style.fontSize,
|
||||
);
|
||||
@@ -1032,18 +1018,12 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
return;
|
||||
}
|
||||
if (widget.message.status == MessageSendingStatus.failed_update) {
|
||||
StreamChat.of(context).client.updateMessage(
|
||||
widget.message,
|
||||
channel.cid,
|
||||
);
|
||||
channel.updateMessage(widget.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.message.status == MessageSendingStatus.failed_delete) {
|
||||
StreamChat.of(context).client.deleteMessage(
|
||||
widget.message,
|
||||
channel.cid,
|
||||
);
|
||||
channel.deleteMessage(widget.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'attachment_error.dart';
|
||||
import 'attachment/attachment.dart';
|
||||
import 'extension.dart';
|
||||
import 'image_attachment.dart';
|
||||
import 'message_text.dart';
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'user_avatar.dart';
|
||||
@@ -200,10 +199,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
);
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
|
||||
Widget _parseAttachments(BuildContext context) {
|
||||
@@ -231,8 +227,8 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
type: MaterialType.transparency,
|
||||
shape: attachment.type == 'file' ? null : _getDefaultShape(context),
|
||||
child: child,
|
||||
),
|
||||
@@ -294,10 +290,9 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
},
|
||||
imageUrl:
|
||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
errorWidget: (context, url, error) {
|
||||
return AttachmentError(size: size);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -901,4 +901,16 @@ class StreamSvgIcon extends StatelessWidget {
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.retry({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'icon_retry.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
class UploadProgressIndicator extends StatelessWidget {
|
||||
final int uploaded;
|
||||
final int total;
|
||||
final Color progressIndicatorColor;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final bool showBackground;
|
||||
final TextStyle textStyle;
|
||||
|
||||
const UploadProgressIndicator({
|
||||
Key key,
|
||||
@required this.uploaded,
|
||||
@required this.total,
|
||||
this.progressIndicatorColor = const Color(0xffb2b2b2),
|
||||
this.padding = const EdgeInsets.only(top: 5, bottom: 5, right: 11, left: 5),
|
||||
this.showBackground = true,
|
||||
this.textStyle,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
Widget child = Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(progressIndicatorColor),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'${fileSize(uploaded, 1)}/${fileSize(total, 1)}',
|
||||
style: textStyle ??
|
||||
theme.textTheme.footnote.copyWith(
|
||||
color: theme.colorTheme.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (showBackground) {
|
||||
child = Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorTheme.overlayDark.withOpacity(0.6),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'stream_svg_icon.dart';
|
||||
import 'dart:math';
|
||||
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
@@ -215,7 +216,7 @@ String getWebsiteName(String hostName) {
|
||||
}
|
||||
|
||||
/// A method returns a human readable string representing a file _size
|
||||
String filesize(dynamic size, [int round = 2]) {
|
||||
String fileSize(dynamic size, [int round = 2]) {
|
||||
if (size == null) return 'Size N/A';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'attachment_title.dart';
|
||||
|
||||
class VideoAttachment extends StatefulWidget {
|
||||
final Attachment attachment;
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final Message message;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
final VideoPackage videoPackage;
|
||||
|
||||
VideoAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
@required this.messageTheme,
|
||||
this.videoPackage,
|
||||
this.message,
|
||||
this.size,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_VideoAttachmentState createState() => _VideoAttachmentState();
|
||||
}
|
||||
|
||||
class _VideoAttachmentState extends State<VideoAttachment> {
|
||||
bool initialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.videoPackage.onInit = () {
|
||||
setState(() {
|
||||
initialized = true;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.videoPackage.initialised) {
|
||||
return Container(
|
||||
height: widget.size?.height ?? 100,
|
||||
width: widget.size?.width ?? 100,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
var res = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [widget.attachment],
|
||||
userName: widget.message.user.name,
|
||||
sentAt: widget.message.createdAt,
|
||||
message: widget.message,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (res != null) {
|
||||
widget.onReturnAction(res);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: widget.size?.height,
|
||||
width: widget.size?.width,
|
||||
child: Flex(
|
||||
direction: Axis.vertical,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.none,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Chewie(
|
||||
controller: widget.videoPackage.chewieController,
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Material(
|
||||
shape: CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Icon(Icons.play_arrow),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.attachment.title != null)
|
||||
Material(
|
||||
color: widget.messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
messageTheme: widget.messageTheme,
|
||||
attachment: widget.attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
class IVideoService {
|
||||
static final IVideoService instance = IVideoService._();
|
||||
final _lock = Lock();
|
||||
|
||||
IVideoService._();
|
||||
|
||||
/// compress video from [path]
|
||||
/// compress video from [path] return [Future<MediaInfo>]
|
||||
///
|
||||
/// you can choose its quality by [quality],
|
||||
/// determine whether to delete his source file by [deleteOrigin]
|
||||
/// optional parameters [startTime] [duration] [includeAudio] [frameRate]
|
||||
///
|
||||
/// ## example
|
||||
/// ```dart
|
||||
/// final info = await _flutterVideoCompress.compressVideo(
|
||||
/// file.path,
|
||||
/// deleteOrigin: true,
|
||||
/// );
|
||||
/// debugPrint(info.toJson());
|
||||
/// ```
|
||||
Future<MediaInfo> compressVideo(String path) async {
|
||||
return _lock.synchronized(() {
|
||||
return VideoCompress.compressVideo(
|
||||
path,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Generates a thumbnail image data in memory as UInt8List, it can be easily used by Image.memory(...).
|
||||
/// The video can be a local video file, or an URL repreents iOS or Android native supported video format.
|
||||
/// Speicify the maximum height or width for the thumbnail or 0 for same resolution as the original video.
|
||||
/// The lower quality value creates lower quality of the thumbnail image, but it gets ignored for PNG format.
|
||||
Future<Uint8List> generateVideoThumbnail({
|
||||
@required String video,
|
||||
ImageFormat imageFormat = ImageFormat.PNG,
|
||||
int maxHeight = 0,
|
||||
int maxWidth = 0,
|
||||
int timeMs = 0,
|
||||
int quality = 10,
|
||||
}) {
|
||||
return VideoThumbnail.thumbnailData(
|
||||
video: video,
|
||||
imageFormat: imageFormat,
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: maxWidth,
|
||||
timeMs: timeMs,
|
||||
quality: quality,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
IVideoService get VideoService => IVideoService.instance;
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
|
||||
import 'stream_svg_icon.dart';
|
||||
import 'video_service.dart';
|
||||
|
||||
class VideoThumbnailImage extends StatefulWidget {
|
||||
final String video;
|
||||
final double width;
|
||||
final double height;
|
||||
final BoxFit fit;
|
||||
final ImageFormat format;
|
||||
final Widget Function(BuildContext, Object) errorBuilder;
|
||||
final WidgetBuilder placeholderBuilder;
|
||||
|
||||
const VideoThumbnailImage({
|
||||
Key key,
|
||||
@required this.video,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit,
|
||||
this.format = ImageFormat.PNG,
|
||||
this.errorBuilder,
|
||||
this.placeholderBuilder,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_VideoThumbnailImageState createState() => _VideoThumbnailImageState();
|
||||
}
|
||||
|
||||
class _VideoThumbnailImageState extends State<VideoThumbnailImage> {
|
||||
Future<Uint8List> thumbnailFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
thumbnailFuture = VideoService.generateVideoThumbnail(
|
||||
video: widget.video,
|
||||
imageFormat: widget.format,
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant VideoThumbnailImage oldWidget) {
|
||||
if (oldWidget.video != widget.video || oldWidget.format != widget.format) {
|
||||
thumbnailFuture = VideoService.generateVideoThumbnail(
|
||||
video: widget.video,
|
||||
imageFormat: widget.format,
|
||||
);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Uint8List>(
|
||||
future: thumbnailFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
if (widget.errorBuilder != null) {
|
||||
return widget.errorBuilder(context, snapshot.error);
|
||||
}
|
||||
return Center(child: StreamSvgIcon.error());
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
if (widget.placeholderBuilder != null) {
|
||||
return widget.placeholderBuilder(context);
|
||||
}
|
||||
return Image.asset(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
fit: widget.fit,
|
||||
);
|
||||
}
|
||||
final data = snapshot.data;
|
||||
return Image.memory(
|
||||
data,
|
||||
fit: widget.fit,
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,10 @@ export 'src/channel_name.dart';
|
||||
export 'src/channel_preview.dart';
|
||||
export 'src/date_divider.dart';
|
||||
export 'src/deleted_message.dart';
|
||||
export 'src/file_attachment.dart';
|
||||
export 'src/attachment/attachment.dart';
|
||||
export 'src/full_screen_media.dart';
|
||||
export 'src/image_header.dart';
|
||||
export 'src/image_footer.dart';
|
||||
export 'src/giphy_attachment.dart';
|
||||
export 'src/image_attachment.dart';
|
||||
export 'src/message_input.dart';
|
||||
export 'src/message_list_view.dart';
|
||||
export 'src/message_text.dart';
|
||||
@@ -31,7 +29,6 @@ export 'src/user_item.dart';
|
||||
export 'src/user_list_view.dart';
|
||||
export 'src/user_list_view.dart';
|
||||
export 'src/utils.dart';
|
||||
export 'src/video_attachment.dart';
|
||||
export 'src/message_search_item.dart';
|
||||
export 'src/message_search_list_view.dart';
|
||||
export 'src/unread_indicator.dart';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="19" viewBox="0 0 24 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.96 2C8.26563 2 5.20156 4.75636 4.71677 8.32007H6.31992C7.10992 8.32007 7.57992 9.21007 7.12992 9.87007L4.45992 13.7401C4.06992 14.3101 3.22992 14.3101 2.83992 13.7401L0.179922 9.87007C-0.280078 9.21007 0.189922 8.32007 0.989922 8.32007H2.70214C3.19783 3.6451 7.16369 0 11.96 0C13.76 0 15.5 0.51 17.01 1.49L15.92 3.17C14.74 2.4 13.37 2 11.96 2ZM6.90991 17.1201C8.41991 18.1001 10.1599 18.6101 11.9599 18.6101C16.7556 18.6101 20.7211 14.9659 21.2175 10.2999H22.9299C23.7299 10.2999 24.1999 9.40989 23.7499 8.74989L21.0799 4.87989C20.6899 4.30989 19.8499 4.30989 19.4599 4.87989L16.7899 8.74989C16.3399 9.40989 16.8099 10.2999 17.5999 10.2999H19.2031C18.7184 13.8637 15.6543 16.6201 11.9599 16.6201C10.5499 16.6201 9.17991 16.2101 7.99991 15.4501L6.90991 17.1201Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 954 B |
@@ -47,6 +47,7 @@ dependencies:
|
||||
characters: ^1.0.0
|
||||
dio: ^3.0.10
|
||||
path_provider: ^1.6.27
|
||||
video_thumbnail: ^0.2.5+1
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
|
||||
@@ -11,7 +11,8 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
cupertino_icons: ^1.0.0
|
||||
stream_chat: ^1.0.0-beta
|
||||
stream_chat:
|
||||
path: ../../stream_chat
|
||||
stream_chat_persistence:
|
||||
path: ../
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ extension MessageEntityX on MessageEntity {
|
||||
ownReactions: ownReactions,
|
||||
attachments: attachments?.map((it) {
|
||||
final json = jsonDecode(it);
|
||||
return Attachment.fromJson(json);
|
||||
return Attachment.fromData(json);
|
||||
})?.toList(),
|
||||
createdAt: createdAt,
|
||||
extraData: extraData,
|
||||
@@ -47,7 +47,9 @@ extension MessageX on Message {
|
||||
MessageEntity toEntity({String cid}) {
|
||||
return MessageEntity(
|
||||
id: id,
|
||||
attachments: attachments?.map((it) => jsonEncode(it))?.toList() ?? [],
|
||||
attachments: attachments?.map((it) {
|
||||
return jsonEncode(it.toData());
|
||||
})?.toList(),
|
||||
channelCid: cid,
|
||||
type: type,
|
||||
parentId: parentId,
|
||||
|
||||
Reference in New Issue
Block a user