[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:
|
analyzer:
|
||||||
exclude:
|
exclude:
|
||||||
- lib/**/*.g.dart
|
- lib/**/*.g.dart
|
||||||
|
- lib/**/*.freezed.dart
|
||||||
- example/*
|
- example/*
|
||||||
- test/*
|
- test/*
|
||||||
|
|
||||||
|
|||||||
+35
-11
@@ -1,56 +1,80 @@
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat/src/models/attachment_file.dart';
|
||||||
import 'extension.dart';
|
import '../client.dart';
|
||||||
|
import '../extensions/string_extension.dart';
|
||||||
|
|
||||||
|
///
|
||||||
abstract class AttachmentUploader {
|
abstract class AttachmentUploader {
|
||||||
|
///
|
||||||
Future<String> uploadImage(
|
Future<String> uploadImage(
|
||||||
PlatformFile file, {
|
AttachmentFile file,
|
||||||
|
String channelId,
|
||||||
|
String channelType, {
|
||||||
ProgressCallback onSendProgress,
|
ProgressCallback onSendProgress,
|
||||||
|
CancelToken cancelToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
///
|
||||||
Future<String> uploadFile(
|
Future<String> uploadFile(
|
||||||
PlatformFile file, {
|
AttachmentFile file,
|
||||||
|
String channelId,
|
||||||
|
String channelType, {
|
||||||
ProgressCallback onSendProgress,
|
ProgressCallback onSendProgress,
|
||||||
|
CancelToken cancelToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
class StreamAttachmentUploader implements AttachmentUploader {
|
class StreamAttachmentUploader implements AttachmentUploader {
|
||||||
final Channel _channel;
|
final StreamChatClient _client;
|
||||||
|
|
||||||
const StreamAttachmentUploader(this._channel);
|
///
|
||||||
|
const StreamAttachmentUploader(this._client);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> uploadImage(
|
Future<String> uploadImage(
|
||||||
PlatformFile file, {
|
AttachmentFile file,
|
||||||
|
String channelId,
|
||||||
|
String channelType, {
|
||||||
ProgressCallback onSendProgress,
|
ProgressCallback onSendProgress,
|
||||||
|
CancelToken cancelToken,
|
||||||
}) async {
|
}) async {
|
||||||
final filename = file.path?.split('/')?.last;
|
final filename = file.path?.split('/')?.last;
|
||||||
final mimeType = filename.mimeType;
|
final mimeType = filename.mimeType;
|
||||||
final res = await _channel.sendImage(
|
final res = await _client.sendImage(
|
||||||
await MultipartFile.fromFile(
|
await MultipartFile.fromFile(
|
||||||
file.path,
|
file.path,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
contentType: mimeType,
|
contentType: mimeType,
|
||||||
),
|
),
|
||||||
|
channelId,
|
||||||
|
channelType,
|
||||||
onSendProgress: onSendProgress,
|
onSendProgress: onSendProgress,
|
||||||
|
cancelToken: cancelToken,
|
||||||
);
|
);
|
||||||
return res.file;
|
return res.file;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> uploadFile(
|
Future<String> uploadFile(
|
||||||
PlatformFile file, {
|
AttachmentFile file,
|
||||||
|
String channelId,
|
||||||
|
String channelType, {
|
||||||
ProgressCallback onSendProgress,
|
ProgressCallback onSendProgress,
|
||||||
|
CancelToken cancelToken,
|
||||||
}) async {
|
}) async {
|
||||||
final filename = file.path?.split('/')?.last;
|
final filename = file.path?.split('/')?.last;
|
||||||
final mimeType = filename.mimeType;
|
final mimeType = filename.mimeType;
|
||||||
final res = await _channel.sendFile(
|
final res = await _client.sendFile(
|
||||||
await MultipartFile.fromFile(
|
await MultipartFile.fromFile(
|
||||||
file.path,
|
file.path,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
contentType: mimeType,
|
contentType: mimeType,
|
||||||
),
|
),
|
||||||
|
channelId,
|
||||||
|
channelType,
|
||||||
onSendProgress: onSendProgress,
|
onSendProgress: onSendProgress,
|
||||||
|
cancelToken: cancelToken,
|
||||||
);
|
);
|
||||||
return res.file;
|
return res.file;
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,16 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:pedantic/pedantic.dart' show unawaited;
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:stream_chat/src/api/retry_queue.dart';
|
import 'package:stream_chat/src/api/retry_queue.dart';
|
||||||
import 'package:stream_chat/src/event_type.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/channel_state.dart';
|
||||||
import 'package:stream_chat/src/models/user.dart';
|
import 'package:stream_chat/src/models/user.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
import '../client.dart';
|
import '../client.dart';
|
||||||
import '../models/event.dart';
|
import '../models/event.dart';
|
||||||
@@ -171,19 +172,148 @@ class Channel {
|
|||||||
/// Call [watch] to initialize the client or instantiate it using [Channel.fromState]
|
/// Call [watch] to initialize the client or instantiate it using [Channel.fromState]
|
||||||
Future<bool> get initialized => _initializedCompleter.future;
|
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 {
|
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(
|
final quotedMessage = state?.messages?.firstWhere(
|
||||||
(m) => m.id == message?.quotedMessageId,
|
(m) => m.id == message?.quotedMessageId,
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
final newMessage = message.copyWith(
|
message = message.copyWith(
|
||||||
createdAt: message.createdAt ?? DateTime.now(),
|
createdAt: message.createdAt ?? DateTime.now(),
|
||||||
user: _client.state.user,
|
user: _client.state.user,
|
||||||
id: messageId,
|
|
||||||
quotedMessage: quotedMessage,
|
quotedMessage: quotedMessage,
|
||||||
status: MessageSendingStatus.sending,
|
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) {
|
if (message.parentId != null && message.id == null) {
|
||||||
@@ -195,17 +325,24 @@ class Channel {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
state?.addMessage(newMessage);
|
state?.addMessage(message);
|
||||||
|
|
||||||
try {
|
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(
|
final response = await _client.post(
|
||||||
'$_channelURL/message',
|
'$_channelURL/message',
|
||||||
data: {
|
data: {
|
||||||
'message': message
|
'message': message.toJson(),
|
||||||
.copyWith(
|
|
||||||
id: messageId,
|
|
||||||
)
|
|
||||||
.toJson()
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -216,7 +353,104 @@ class Channel {
|
|||||||
return res;
|
return res;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error is DioError && error.type != DioErrorType.RESPONSE) {
|
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;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -226,26 +460,30 @@ class Channel {
|
|||||||
Future<SendFileResponse> sendFile(
|
Future<SendFileResponse> sendFile(
|
||||||
MultipartFile file, {
|
MultipartFile file, {
|
||||||
ProgressCallback onSendProgress,
|
ProgressCallback onSendProgress,
|
||||||
}) async {
|
CancelToken cancelToken,
|
||||||
final response = await _client.post(
|
}) {
|
||||||
'$_channelURL/file',
|
return _client.sendFile(
|
||||||
data: FormData.fromMap({'file': file}),
|
file,
|
||||||
|
id,
|
||||||
|
type,
|
||||||
onSendProgress: onSendProgress,
|
onSendProgress: onSendProgress,
|
||||||
|
cancelToken: cancelToken,
|
||||||
);
|
);
|
||||||
return _client.decode(response.data, SendFileResponse.fromJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send an image to this channel
|
/// Send an image to this channel
|
||||||
Future<SendImageResponse> sendImage(
|
Future<SendImageResponse> sendImage(
|
||||||
MultipartFile file, {
|
MultipartFile image, {
|
||||||
ProgressCallback onSendProgress,
|
ProgressCallback onSendProgress,
|
||||||
}) async {
|
CancelToken cancelToken,
|
||||||
final response = await _client.post(
|
}) {
|
||||||
'$_channelURL/image',
|
return _client.sendImage(
|
||||||
data: FormData.fromMap({'file': file}),
|
image,
|
||||||
|
id,
|
||||||
|
type,
|
||||||
onSendProgress: onSendProgress,
|
onSendProgress: onSendProgress,
|
||||||
|
cancelToken: cancelToken,
|
||||||
);
|
);
|
||||||
return _client.decode(response.data, SendImageResponse.fromJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a file from this channel
|
/// Delete a file from this channel
|
||||||
@@ -975,7 +1213,13 @@ class ChannelClientState {
|
|||||||
?.getChannelThreads(_channel.cid)
|
?.getChannelThreads(_channel.cid)
|
||||||
?.then((threads) {
|
?.then((threads) {
|
||||||
_threads = 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 {
|
Future<void> _sendMessage(Message message) async {
|
||||||
if (message.status == MessageSendingStatus.failed_update ||
|
if (message.status == MessageSendingStatus.failed_update ||
|
||||||
message.status == MessageSendingStatus.updating) {
|
message.status == MessageSendingStatus.updating) {
|
||||||
await channel.client.updateMessage(
|
await channel.updateMessage(message);
|
||||||
message,
|
|
||||||
channel.cid,
|
|
||||||
);
|
|
||||||
} else if (message.status == MessageSendingStatus.failed ||
|
} else if (message.status == MessageSendingStatus.failed ||
|
||||||
message.status == MessageSendingStatus.sending) {
|
message.status == MessageSendingStatus.sending) {
|
||||||
await channel.sendMessage(
|
await channel.sendMessage(
|
||||||
@@ -136,10 +133,7 @@ class RetryQueue {
|
|||||||
);
|
);
|
||||||
} else if (message.status == MessageSendingStatus.failed_delete ||
|
} else if (message.status == MessageSendingStatus.failed_delete ||
|
||||||
message.status == MessageSendingStatus.deleting) {
|
message.status == MessageSendingStatus.deleting) {
|
||||||
await channel.client.deleteMessage(
|
await channel.client.deleteMessage(message);
|
||||||
message,
|
|
||||||
channel.cid,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:stream_chat/src/models/own_user.dart';
|
|||||||
import 'package:stream_chat/version.dart';
|
import 'package:stream_chat/version.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import 'api/attachment_uploader.dart';
|
||||||
import 'api/channel.dart';
|
import 'api/channel.dart';
|
||||||
import 'api/connection_status.dart';
|
import 'api/connection_status.dart';
|
||||||
import 'api/requests.dart';
|
import 'api/requests.dart';
|
||||||
@@ -78,6 +79,7 @@ class StreamChatClient {
|
|||||||
Duration receiveTimeout = const Duration(seconds: 6),
|
Duration receiveTimeout = const Duration(seconds: 6),
|
||||||
Dio httpClient,
|
Dio httpClient,
|
||||||
RetryPolicy retryPolicy,
|
RetryPolicy retryPolicy,
|
||||||
|
this.attachmentUploader,
|
||||||
}) {
|
}) {
|
||||||
_retryPolicy ??= RetryPolicy(
|
_retryPolicy ??= RetryPolicy(
|
||||||
retryTimeout: (StreamChatClient client, int attempt, ApiError error) =>
|
retryTimeout: (StreamChatClient client, int attempt, ApiError error) =>
|
||||||
@@ -86,6 +88,8 @@ class StreamChatClient {
|
|||||||
attempt < 5,
|
attempt < 5,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
attachmentUploader ??= StreamAttachmentUploader(this);
|
||||||
|
|
||||||
state = ClientState(this);
|
state = ClientState(this);
|
||||||
|
|
||||||
_setupLogger();
|
_setupLogger();
|
||||||
@@ -97,6 +101,9 @@ class StreamChatClient {
|
|||||||
/// Chat persistence client
|
/// Chat persistence client
|
||||||
ChatPersistenceClient chatPersistenceClient;
|
ChatPersistenceClient chatPersistenceClient;
|
||||||
|
|
||||||
|
/// Attachment uploader
|
||||||
|
AttachmentUploader attachmentUploader;
|
||||||
|
|
||||||
/// Whether the chat persistence is available or not
|
/// Whether the chat persistence is available or not
|
||||||
bool get persistenceEnabled => chatPersistenceClient != null;
|
bool get persistenceEnabled => chatPersistenceClient != null;
|
||||||
|
|
||||||
@@ -795,12 +802,14 @@ class StreamChatClient {
|
|||||||
String path, {
|
String path, {
|
||||||
dynamic data,
|
dynamic data,
|
||||||
ProgressCallback onSendProgress,
|
ProgressCallback onSendProgress,
|
||||||
|
CancelToken cancelToken,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final response = await httpClient.post<String>(
|
final response = await httpClient.post<String>(
|
||||||
path,
|
path,
|
||||||
data: data,
|
data: data,
|
||||||
onSendProgress: onSendProgress,
|
onSendProgress: onSendProgress,
|
||||||
|
cancelToken: cancelToken,
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
} on DioError catch (error) {
|
} on DioError catch (error) {
|
||||||
@@ -1030,6 +1039,40 @@ class StreamChatClient {
|
|||||||
response.data, SearchMessagesResponse.fromJson);
|
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.
|
/// Add a device for Push Notifications.
|
||||||
Future<EmptyResponse> addDevice(String id, PushProvider pushProvider) async {
|
Future<EmptyResponse> addDevice(String id, PushProvider pushProvider) async {
|
||||||
final response = await post('/devices', data: {
|
final response = await post('/devices', data: {
|
||||||
@@ -1202,77 +1245,18 @@ class StreamChatClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update the given message
|
/// Update the given message
|
||||||
Future<UpdateMessageResponse> updateMessage(
|
Future<UpdateMessageResponse> updateMessage(Message message) async {
|
||||||
Message message, [
|
final response = await post(
|
||||||
String cid,
|
'/messages/${message.id}',
|
||||||
]) async {
|
data: {'message': message},
|
||||||
message = message.copyWith(
|
|
||||||
status: MessageSendingStatus.updating,
|
|
||||||
updatedAt: message.updatedAt ?? DateTime.now(),
|
|
||||||
);
|
);
|
||||||
|
return decode(response.data, UpdateMessageResponse.fromJson);
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the given message
|
/// Deletes the given message
|
||||||
Future<EmptyResponse> deleteMessage(Message message, [String cid]) async {
|
Future<EmptyResponse> deleteMessage(Message message) async {
|
||||||
if (message.status == MessageSendingStatus.failed) {
|
final response = await delete('/messages/${message.id}');
|
||||||
state.channels[cid].state.addMessage(message.copyWith(
|
return decode(response.data, EmptyResponse.fromJson);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a message by id
|
/// 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
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
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 'action.dart';
|
||||||
import 'serialization.dart';
|
import 'serialization.dart';
|
||||||
@@ -52,10 +54,21 @@ class Attachment {
|
|||||||
|
|
||||||
final Uri localUri;
|
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
|
/// Map of custom channel extraData
|
||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
final Map<String, dynamic> extraData;
|
final Map<String, dynamic> extraData;
|
||||||
|
|
||||||
|
/// The attachment ID.
|
||||||
|
///
|
||||||
|
/// This is created locally for uniquely identifying a attachment.
|
||||||
|
final String id;
|
||||||
|
|
||||||
/// Known top level fields.
|
/// Known top level fields.
|
||||||
/// Useful for [Serialization] methods.
|
/// Useful for [Serialization] methods.
|
||||||
static const topLevelFields = [
|
static const topLevelFields = [
|
||||||
@@ -79,11 +92,20 @@ class Attachment {
|
|||||||
'actions',
|
'actions',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Known db specific top level fields.
|
||||||
|
/// Useful for [Serialization] methods.
|
||||||
|
static const dbSpecificTopLevelFields = [
|
||||||
|
'id',
|
||||||
|
'upload_state',
|
||||||
|
'file',
|
||||||
|
];
|
||||||
|
|
||||||
/// Constructor used for json serialization
|
/// Constructor used for json serialization
|
||||||
Attachment({
|
Attachment({
|
||||||
|
String id,
|
||||||
this.type,
|
this.type,
|
||||||
this.titleLink,
|
this.titleLink,
|
||||||
this.title,
|
String title,
|
||||||
this.thumbUrl,
|
this.thumbUrl,
|
||||||
this.text,
|
this.text,
|
||||||
this.pretext,
|
this.pretext,
|
||||||
@@ -100,8 +122,11 @@ class Attachment {
|
|||||||
this.assetUrl,
|
this.assetUrl,
|
||||||
this.actions,
|
this.actions,
|
||||||
this.extraData,
|
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
|
/// Create a new instance from a json
|
||||||
factory Attachment.fromJson(Map<String, dynamic> json) {
|
factory Attachment.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -111,9 +136,21 @@ class Attachment {
|
|||||||
|
|
||||||
/// Serialize to json
|
/// Serialize to json
|
||||||
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
|
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({
|
Attachment copyWith({
|
||||||
|
String id,
|
||||||
String type,
|
String type,
|
||||||
String titleLink,
|
String titleLink,
|
||||||
String title,
|
String title,
|
||||||
@@ -132,10 +169,12 @@ class Attachment {
|
|||||||
String authorIcon,
|
String authorIcon,
|
||||||
String assetUrl,
|
String assetUrl,
|
||||||
List<Action> actions,
|
List<Action> actions,
|
||||||
Uri localUri,
|
AttachmentFile file,
|
||||||
|
UploadState uploadState,
|
||||||
Map<String, dynamic> extraData,
|
Map<String, dynamic> extraData,
|
||||||
}) =>
|
}) =>
|
||||||
Attachment(
|
Attachment(
|
||||||
|
id: id ?? this.id,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
titleLink: titleLink ?? this.titleLink,
|
titleLink: titleLink ?? this.titleLink,
|
||||||
title: title ?? this.title,
|
title: title ?? this.title,
|
||||||
@@ -154,7 +193,8 @@ class Attachment {
|
|||||||
authorIcon: authorIcon ?? this.authorIcon,
|
authorIcon: authorIcon ?? this.authorIcon,
|
||||||
assetUrl: assetUrl ?? this.assetUrl,
|
assetUrl: assetUrl ?? this.assetUrl,
|
||||||
actions: actions ?? this.actions,
|
actions: actions ?? this.actions,
|
||||||
localUri: localUri ?? this.localUri,
|
file: file ?? this.file,
|
||||||
|
uploadState: uploadState ?? this.uploadState,
|
||||||
extraData: extraData ?? this.extraData,
|
extraData: extraData ?? this.extraData,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ part of 'attachment.dart';
|
|||||||
|
|
||||||
Attachment _$AttachmentFromJson(Map json) {
|
Attachment _$AttachmentFromJson(Map json) {
|
||||||
return Attachment(
|
return Attachment(
|
||||||
|
id: json['id'] as String,
|
||||||
type: json['type'] as String,
|
type: json['type'] as String,
|
||||||
titleLink: json['title_link'] as String,
|
titleLink: json['title_link'] as String,
|
||||||
title: json['title'] as String,
|
title: json['title'] as String,
|
||||||
@@ -35,9 +36,16 @@ Attachment _$AttachmentFromJson(Map json) {
|
|||||||
extraData: (json['extra_data'] as Map)?.map(
|
extraData: (json['extra_data'] as Map)?.map(
|
||||||
(k, e) => MapEntry(k as String, e),
|
(k, e) => MapEntry(k as String, e),
|
||||||
),
|
),
|
||||||
localUri: json['local_uri'] == null
|
file: json['file'] == null
|
||||||
? 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('author_icon', instance.authorIcon);
|
||||||
writeNotNull('asset_url', instance.assetUrl);
|
writeNotNull('asset_url', instance.assetUrl);
|
||||||
writeNotNull('actions', instance.actions?.map((e) => e?.toJson())?.toList());
|
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('extra_data', instance.extraData);
|
||||||
|
writeNotNull('id', instance.id);
|
||||||
return val;
|
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:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import 'attachment.dart';
|
import 'attachment.dart';
|
||||||
import 'reaction.dart';
|
import 'reaction.dart';
|
||||||
@@ -163,7 +164,7 @@ class Message {
|
|||||||
|
|
||||||
/// Constructor used for json serialization
|
/// Constructor used for json serialization
|
||||||
Message({
|
Message({
|
||||||
this.id,
|
String id,
|
||||||
this.text,
|
this.text,
|
||||||
this.type,
|
this.type,
|
||||||
this.attachments,
|
this.attachments,
|
||||||
@@ -187,7 +188,7 @@ class Message {
|
|||||||
this.extraData,
|
this.extraData,
|
||||||
this.deletedAt,
|
this.deletedAt,
|
||||||
this.status = MessageSendingStatus.sent,
|
this.status = MessageSendingStatus.sent,
|
||||||
});
|
}) : id = id ?? Uuid().v4();
|
||||||
|
|
||||||
/// Create a new instance from a json
|
/// Create a new instance from a json
|
||||||
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
|
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/requests.dart';
|
export './src/api/requests.dart';
|
||||||
export './src/api/responses.dart';
|
export './src/api/responses.dart';
|
||||||
|
export './src/api/attachment_uploader.dart' show AttachmentUploader;
|
||||||
export './src/client.dart';
|
export './src/client.dart';
|
||||||
export './src/event_type.dart';
|
export './src/event_type.dart';
|
||||||
export './src/models/action.dart';
|
export './src/models/action.dart';
|
||||||
export './src/models/attachment.dart';
|
export './src/models/attachment.dart';
|
||||||
|
export './src/models/attachment_file.dart';
|
||||||
export './src/models/channel_config.dart';
|
export './src/models/channel_config.dart';
|
||||||
export './src/models/channel_model.dart';
|
export './src/models/channel_model.dart';
|
||||||
export './src/models/channel_state.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/reaction.dart';
|
||||||
export './src/models/read.dart';
|
export './src/models/read.dart';
|
||||||
export './src/models/user.dart';
|
export './src/models/user.dart';
|
||||||
|
export './src/extensions/string_extension.dart';
|
||||||
export './src/db/chat_persistence_client.dart';
|
export './src/db/chat_persistence_client.dart';
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ dependencies:
|
|||||||
collection: ^1.14.13
|
collection: ^1.14.13
|
||||||
pedantic: ^1.9.2
|
pedantic: ^1.9.2
|
||||||
meta: ^1.2.4
|
meta: ^1.2.4
|
||||||
|
freezed_annotation: ^0.12.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^1.10.0
|
build_runner: ^1.10.0
|
||||||
json_serializable: ^3.3.0
|
json_serializable: ^3.3.0
|
||||||
test: ^1.15.7
|
test: ^1.15.7
|
||||||
mockito: ^4.1.1
|
mockito: ^4.1.1
|
||||||
|
freezed: ^0.12.7
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
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 */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
@@ -31,7 +32,11 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -49,12 +54,24 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
1589B944F51366A883B3A7A5 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup 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 */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -72,6 +89,8 @@
|
|||||||
9740EEB11CF90186004384FC /* Flutter */,
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
38CA51382F78F25FC59B6C81 /* Pods */,
|
||||||
|
E3E00C71A36D1ABA459667BF /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -98,6 +117,14 @@
|
|||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E3E00C71A36D1ABA459667BF /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4AFDB673F1C9808CE4EC418F /* Pods_Runner.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -105,12 +132,14 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
41D6CB24535AC541800DBB07 /* [CP] Check Pods Manifest.lock */,
|
||||||
9740EEB61CF901F6004384FC /* Run Script */,
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
9DF0031578B883CBE79BDCC8 /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -183,6 +212,28 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
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 */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -197,6 +248,23 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
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 */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
|||||||
+3
@@ -4,4 +4,7 @@
|
|||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Runner.xcodeproj">
|
location = "group:Runner.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
|
||||||
|
|
||||||
|
final chatPersistentClient = StreamChatPersistenceClient(
|
||||||
|
logLevel: Level.INFO,
|
||||||
|
connectionMode: ConnectionMode.background,
|
||||||
|
);
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
/// Create a new instance of [StreamChatClient] passing the apikey obtained from your
|
/// Create a new instance of [StreamChatClient] passing the apikey obtained from your
|
||||||
/// project dashboard.
|
/// project dashboard.
|
||||||
final client = StreamChatClient(
|
final client = StreamChatClient(
|
||||||
's2dxdhpxd94g',
|
's2dxdhpxd94g',
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
);
|
)..chatPersistenceClient = chatPersistentClient;
|
||||||
|
|
||||||
/// Set the current user and connect the websocket. In a production scenario, this should be done using
|
/// 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.
|
/// 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');
|
final channel = client.channel('messaging', id: 'godevs');
|
||||||
|
|
||||||
// ignore: unawaited_futures
|
await channel.watch();
|
||||||
channel.watch();
|
|
||||||
|
|
||||||
runApp(MyApp(client, channel));
|
runApp(MyApp(client, channel));
|
||||||
}
|
}
|
||||||
@@ -49,6 +56,9 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
theme: ThemeData.light(),
|
||||||
|
darkTheme: ThemeData.dark(),
|
||||||
|
themeMode: ThemeMode.system,
|
||||||
builder: (context, widget) {
|
builder: (context, widget) {
|
||||||
return StreamChat(
|
return StreamChat(
|
||||||
child: widget,
|
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:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
import 'stream_chat_theme.dart';
|
import '../stream_chat_theme.dart';
|
||||||
import 'utils.dart';
|
import '../utils.dart';
|
||||||
|
|
||||||
class AttachmentTitle extends StatelessWidget {
|
class AttachmentTitle extends StatelessWidget {
|
||||||
const AttachmentTitle({
|
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:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.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 '../full_screen_media.dart';
|
||||||
import 'attachment_error.dart';
|
import '../stream_chat_theme.dart';
|
||||||
import 'full_screen_media.dart';
|
import '../stream_svg_icon.dart';
|
||||||
|
import 'attachment_widget.dart';
|
||||||
|
|
||||||
class GiphyAttachment extends StatelessWidget {
|
class GiphyAttachment extends AttachmentWidget {
|
||||||
final Attachment attachment;
|
|
||||||
final MessageTheme messageTheme;
|
final MessageTheme messageTheme;
|
||||||
final Message message;
|
|
||||||
final Size size;
|
|
||||||
final ShowMessageCallback onShowMessage;
|
final ShowMessageCallback onShowMessage;
|
||||||
final ValueChanged<ReturnActionType> onReturnAction;
|
final ValueChanged<ReturnActionType> onReturnAction;
|
||||||
|
|
||||||
const GiphyAttachment({
|
const GiphyAttachment({
|
||||||
Key key,
|
Key key,
|
||||||
this.attachment,
|
@required Message message,
|
||||||
|
@required Attachment attachment,
|
||||||
|
Size size,
|
||||||
this.messageTheme,
|
this.messageTheme,
|
||||||
this.message,
|
|
||||||
this.size,
|
|
||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
this.onReturnAction,
|
this.onReturnAction,
|
||||||
}) : super(key: key);
|
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (attachment.thumbUrl == null &&
|
final imageUrl =
|
||||||
attachment.imageUrl == null &&
|
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
||||||
attachment.assetUrl == null) {
|
if (imageUrl == null && source == AttachmentSource.network) {
|
||||||
return AttachmentError(
|
return AttachmentError();
|
||||||
attachment: attachment,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
if (attachment.actions != null) {
|
||||||
return attachment.actions != null
|
return _buildSendingAttachment(context, imageUrl);
|
||||||
? _buildSendingAttachment(context)
|
}
|
||||||
: _buildSentAttachment(context);
|
return _buildSentAttachment(context, imageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSendingAttachment(context) {
|
Widget _buildSendingAttachment(BuildContext context, String imageUrl) {
|
||||||
final streamChannel = StreamChannel.of(context);
|
final streamChannel = StreamChannel.of(context);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -67,9 +61,7 @@ class GiphyAttachment extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () async {
|
onTap: () => _onImageTap(context),
|
||||||
_onImageTap(context);
|
|
||||||
},
|
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(8),
|
topLeft: Radius.circular(8),
|
||||||
@@ -87,13 +79,10 @@ class GiphyAttachment extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
imageUrl: attachment.thumbUrl ??
|
imageUrl: imageUrl,
|
||||||
attachment.imageUrl ??
|
errorWidget: (context, url, error) {
|
||||||
attachment.assetUrl,
|
return AttachmentError(size: size);
|
||||||
errorWidget: (context, url, error) => AttachmentError(
|
},
|
||||||
attachment: attachment,
|
|
||||||
size: size,
|
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -305,44 +294,38 @@ class GiphyAttachment extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onImageTap(BuildContext context) async {
|
void _onImageTap(BuildContext context) async {
|
||||||
var res = await Navigator.push(context, MaterialPageRoute(
|
final res = await Navigator.push(
|
||||||
builder: (_) {
|
context,
|
||||||
final channel = StreamChannel.of(context).channel;
|
MaterialPageRoute(
|
||||||
|
builder: (_) {
|
||||||
return StreamChannel(
|
final channel = StreamChannel.of(context).channel;
|
||||||
channel: channel,
|
return StreamChannel(
|
||||||
child: FullScreenMedia(
|
channel: channel,
|
||||||
mediaAttachments: [
|
child: FullScreenMedia(
|
||||||
attachment,
|
mediaAttachments: [attachment],
|
||||||
],
|
userName: message.user.name,
|
||||||
userName: message.user.name,
|
sentAt: message.createdAt,
|
||||||
sentAt: message.createdAt,
|
message: message,
|
||||||
message: message,
|
onShowMessage: onShowMessage,
|
||||||
onShowMessage: onShowMessage,
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
));
|
);
|
||||||
|
if (res != null) onReturnAction(res);
|
||||||
if (res != null) {
|
|
||||||
onReturnAction(res);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSentAttachment(context) {
|
Widget _buildSentAttachment(BuildContext context, String imageUrl) {
|
||||||
return Container(
|
return Container(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var res =
|
final res =
|
||||||
await Navigator.push(context, MaterialPageRoute(builder: (_) {
|
await Navigator.push(context, MaterialPageRoute(builder: (_) {
|
||||||
var channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
|
|
||||||
return StreamChannel(
|
return StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: FullScreenMedia(
|
child: FullScreenMedia(
|
||||||
mediaAttachments: [
|
mediaAttachments: [attachment],
|
||||||
attachment,
|
|
||||||
],
|
|
||||||
userName: message.user.name,
|
userName: message.user.name,
|
||||||
sentAt: message.createdAt,
|
sentAt: message.createdAt,
|
||||||
message: message,
|
message: message,
|
||||||
@@ -350,10 +333,7 @@ class GiphyAttachment extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
if (res != null) onReturnAction(res);
|
||||||
if (res != null) {
|
|
||||||
onReturnAction(res);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -369,13 +349,10 @@ class GiphyAttachment extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
imageUrl: attachment.thumbUrl ??
|
imageUrl: imageUrl,
|
||||||
attachment.imageUrl ??
|
errorWidget: (context, url, error) {
|
||||||
attachment.assetUrl,
|
return AttachmentError(size: size);
|
||||||
errorWidget: (context, url, error) => AttachmentError(
|
},
|
||||||
attachment: attachment,
|
|
||||||
size: size,
|
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
Positioned(
|
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:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'attachment/attachment.dart';
|
||||||
|
|
||||||
class ChannelFileDisplayScreen extends StatefulWidget {
|
class ChannelFileDisplayScreen extends StatefulWidget {
|
||||||
/// The sorting used for the channels matching the filters.
|
/// The sorting used for the channels matching the filters.
|
||||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||||
@@ -164,6 +166,7 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: FileAttachment(
|
child: FileAttachment(
|
||||||
|
message: media.values.toList()[position],
|
||||||
attachment: media.keys.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:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
import 'attachment/attachment.dart';
|
||||||
|
|
||||||
class ChannelMediaDisplayScreen extends StatefulWidget {
|
class ChannelMediaDisplayScreen extends StatefulWidget {
|
||||||
/// The sorting used for the channels matching the filters.
|
/// The sorting used for the channels matching the filters.
|
||||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
/// 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:characters/characters.dart';
|
||||||
import 'package:emojis/emoji.dart';
|
import 'package:emojis/emoji.dart';
|
||||||
import 'package:http_parser/http_parser.dart' as http_parser;
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
final _emojis = Emoji.all();
|
final _emojis = Emoji.all();
|
||||||
|
|
||||||
@@ -23,16 +23,6 @@ extension StringExtension on String {
|
|||||||
if (characters.length > 3) return false;
|
if (characters.length > 3) return false;
|
||||||
return characters.every((c) => _emojis.map((e) => e.char).contains(c));
|
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
|
/// List extension
|
||||||
@@ -43,3 +33,14 @@ extension IterableX<T> on Iterable<T> {
|
|||||||
yield e;
|
yield e;
|
||||||
}).skip(1).toList(growable: false);
|
}).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:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -48,36 +51,34 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
|
|
||||||
int _currentPage;
|
int _currentPage;
|
||||||
|
|
||||||
List<VideoPackage> videoPackages = [];
|
final videoPackages = <String, VideoPackage>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller =
|
_controller = AnimationController(
|
||||||
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
|
vsync: this,
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
_pageController = PageController(initialPage: widget.startIndex);
|
_pageController = PageController(initialPage: widget.startIndex);
|
||||||
_currentPage = widget.startIndex;
|
_currentPage = widget.startIndex;
|
||||||
widget.mediaAttachments
|
for (final attachment in widget.mediaAttachments) {
|
||||||
.where((element) => element.type == 'video')
|
if (attachment.type != 'video') continue;
|
||||||
.toList()
|
final package = VideoPackage(attachment, showControls: true);
|
||||||
.forEach((element) {
|
videoPackages[attachment.id] = package;
|
||||||
videoPackages.add(VideoPackage(
|
}
|
||||||
context,
|
initializePlayers();
|
||||||
element,
|
}
|
||||||
() {
|
|
||||||
setState(() {});
|
Future<void> initializePlayers() async {
|
||||||
},
|
await Future.wait(videoPackages.values.map(
|
||||||
showControls: true,
|
(it) => it.initialize(),
|
||||||
));
|
));
|
||||||
});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var videoAttachments = widget.mediaAttachments
|
|
||||||
.where((element) => element.type == 'video')
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
@@ -92,14 +93,18 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
_currentPage = val;
|
_currentPage = val;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
itemBuilder: (context, position) {
|
itemBuilder: (context, index) {
|
||||||
if (widget.mediaAttachments[position].type == 'image' ||
|
final attachment = widget.mediaAttachments[index];
|
||||||
widget.mediaAttachments[position].type == 'giphy') {
|
if (attachment.type == 'image' ||
|
||||||
|
attachment.type == 'giphy') {
|
||||||
|
final imageUrl = attachment.imageUrl ??
|
||||||
|
attachment.assetUrl ??
|
||||||
|
attachment.thumbUrl;
|
||||||
return PhotoView(
|
return PhotoView(
|
||||||
imageProvider: CachedNetworkImageProvider(
|
imageProvider:
|
||||||
widget.mediaAttachments[position].imageUrl ??
|
imageUrl == null && attachment.localUri != null
|
||||||
widget.mediaAttachments[position].assetUrl ??
|
? Image.memory(attachment.file.bytes).image
|
||||||
widget.mediaAttachments[position].thumbUrl),
|
: CachedNetworkImageProvider(imageUrl),
|
||||||
maxScale: PhotoViewComputedScale.covered,
|
maxScale: PhotoViewComputedScale.covered,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
heroAttributes: PhotoViewHeroAttributes(
|
||||||
@@ -125,12 +130,9 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if (widget.mediaAttachments[position].type ==
|
} else if (attachment.type == 'video') {
|
||||||
'video') {
|
final controller = videoPackages[attachment.id];
|
||||||
var controllerPackage = videoPackages[videoAttachments
|
if (!controller.initialized) {
|
||||||
.indexOf(widget.mediaAttachments[position])];
|
|
||||||
|
|
||||||
if (!controllerPackage.initialised) {
|
|
||||||
return Center(
|
return Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
@@ -151,7 +153,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
vertical: 50.0,
|
vertical: 50.0,
|
||||||
),
|
),
|
||||||
child: Chewie(
|
child: Chewie(
|
||||||
controller: controllerPackage.chewieController,
|
controller: controller.chewieController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -188,7 +190,6 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
totalPages: widget.mediaAttachments.length,
|
totalPages: widget.mediaAttachments.length,
|
||||||
mediaAttachments: widget.mediaAttachments,
|
mediaAttachments: widget.mediaAttachments,
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
videoPackages: videoPackages,
|
|
||||||
mediaSelectedCallBack: (val) {
|
mediaSelectedCallBack: (val) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentPage = val;
|
_currentPage = val;
|
||||||
@@ -224,54 +225,58 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() async {
|
||||||
videoPackages.forEach((element) {
|
for (final package in videoPackages.values) {
|
||||||
element.dispose();
|
await package.dispose();
|
||||||
});
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoPackage {
|
class VideoPackage {
|
||||||
VideoPlayerController _videoPlayerController;
|
final bool _showControls;
|
||||||
|
final bool _autoInitialize;
|
||||||
|
final VideoPlayerController _videoPlayerController;
|
||||||
ChewieController _chewieController;
|
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;
|
VideoPlayerController get videoPlayer => _videoPlayerController;
|
||||||
|
|
||||||
ChewieController get chewieController => _chewieController;
|
ChewieController get chewieController => _chewieController;
|
||||||
|
|
||||||
void dispose() {
|
bool get initialized => _videoPlayerController.value.initialized;
|
||||||
_videoPlayerController.dispose();
|
|
||||||
_chewieController.dispose();
|
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);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
StreamChat.of(context).client.deleteMessage(
|
StreamChannel.of(context)
|
||||||
message,
|
.channel
|
||||||
StreamChannel.of(context).channel.cid,
|
.deleteMessage(message);
|
||||||
);
|
|
||||||
},
|
},
|
||||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
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 'dart:io';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:chewie/chewie.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.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/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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 List<Attachment> mediaAttachments;
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
final List<VideoPackage> videoPackages;
|
|
||||||
final ValueChanged<int> mediaSelectedCallBack;
|
final ValueChanged<int> mediaSelectedCallBack;
|
||||||
|
|
||||||
/// Creates a channel header
|
/// Creates a channel header
|
||||||
@@ -41,7 +40,6 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
|||||||
this.totalPages = 0,
|
this.totalPages = 0,
|
||||||
this.mediaAttachments,
|
this.mediaAttachments,
|
||||||
this.message,
|
this.message,
|
||||||
this.videoPackages,
|
|
||||||
this.mediaSelectedCallBack,
|
this.mediaSelectedCallBack,
|
||||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
@@ -214,17 +212,14 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
Widget media;
|
Widget media;
|
||||||
final attachment = widget.mediaAttachments[index];
|
final attachment = widget.mediaAttachments[index];
|
||||||
|
|
||||||
if (attachment.type == 'video') {
|
if (attachment.type == 'video') {
|
||||||
var controllerPackage = widget.videoPackages[
|
|
||||||
videoAttachments.indexOf(attachment)];
|
|
||||||
|
|
||||||
media = InkWell(
|
media = InkWell(
|
||||||
onTap: () => widget.mediaSelectedCallBack(index),
|
onTap: () => widget.mediaSelectedCallBack(index),
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
child: Chewie(
|
child: VideoThumbnailImage(
|
||||||
controller: controllerPackage.chewieController,
|
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:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||||
@@ -9,12 +8,14 @@ class ImageGroup extends StatelessWidget {
|
|||||||
Key key,
|
Key key,
|
||||||
@required this.images,
|
@required this.images,
|
||||||
@required this.message,
|
@required this.message,
|
||||||
|
@required this.messageTheme,
|
||||||
@required this.size,
|
@required this.size,
|
||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final List<Attachment> images;
|
final List<Attachment> images;
|
||||||
final Message message;
|
final Message message;
|
||||||
|
final MessageTheme messageTheme;
|
||||||
final Size size;
|
final Size size;
|
||||||
final ShowMessageCallback onShowMessage;
|
final ShowMessageCallback onShowMessage;
|
||||||
|
|
||||||
@@ -129,14 +130,12 @@ class ImageGroup extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImage(BuildContext context, int index) {
|
Widget _buildImage(BuildContext context, int index) {
|
||||||
return GestureDetector(
|
return ImageAttachment(
|
||||||
onTap: () => _onTap(context, index),
|
attachment: images[index],
|
||||||
child: CachedNetworkImage(
|
size: size,
|
||||||
imageUrl: images[index].imageUrl ??
|
message: message,
|
||||||
images[index].thumbUrl ??
|
messageTheme: messageTheme,
|
||||||
images[index].assetUrl,
|
onAttachmentTap: () => _onTap(context, index),
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 messageShape;
|
||||||
final ShapeBorder attachmentShape;
|
final ShapeBorder attachmentShape;
|
||||||
final DisplayWidget showUserAvatar;
|
final DisplayWidget showUserAvatar;
|
||||||
final Map<String, VideoPackage> videoPackages;
|
|
||||||
|
|
||||||
const MessageActionsModal({
|
const MessageActionsModal({
|
||||||
Key key,
|
Key key,
|
||||||
@@ -54,7 +53,6 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
this.messageShape,
|
this.messageShape,
|
||||||
this.attachmentShape,
|
this.attachmentShape,
|
||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
this.videoPackages,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -182,7 +180,6 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
showSendingIndicator: false,
|
showSendingIndicator: false,
|
||||||
shape: widget.messageShape,
|
shape: widget.messageShape,
|
||||||
attachmentShape: widget.attachmentShape,
|
attachmentShape: widget.attachmentShape,
|
||||||
videoPackages: widget.videoPackages,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
@@ -298,10 +295,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
if (answer) {
|
if (answer) {
|
||||||
try {
|
try {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
await StreamChat.of(context).client.deleteMessage(
|
await StreamChannel.of(context).channel.deleteMessage(widget.message);
|
||||||
widget.message,
|
|
||||||
StreamChannel.of(context).channel.cid,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_showErrorAlert();
|
_showErrorAlert();
|
||||||
}
|
}
|
||||||
@@ -570,10 +564,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
final client = StreamChat.of(context).client;
|
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
if (isUpdateFailed) {
|
if (isUpdateFailed) {
|
||||||
client.updateMessage(widget.message, channel.cid);
|
channel.updateMessage(widget.message);
|
||||||
} else {
|
} else {
|
||||||
channel.sendMessage(widget.message);
|
channel.sendMessage(widget.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:emojis/emoji.dart';
|
import 'package:emojis/emoji.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/cupertino.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:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:photo_manager/photo_manager.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/media_list_view.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.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/src/user_avatar.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
import 'package:substring_highlight/substring_highlight.dart';
|
import 'package:substring_highlight/substring_highlight.dart';
|
||||||
import 'package:video_compress/video_compress.dart';
|
|
||||||
|
|
||||||
import '../stream_chat_flutter.dart';
|
import '../stream_chat_flutter.dart';
|
||||||
import 'attachment_uploader.dart';
|
import 'attachment/attachment.dart';
|
||||||
import 'extension.dart';
|
import 'extension.dart';
|
||||||
import 'quoted_message_widget.dart';
|
import 'quoted_message_widget.dart';
|
||||||
|
import 'video_thumbnail_image.dart';
|
||||||
|
|
||||||
typedef AttachmentThumbnailBuilder = Widget Function(
|
typedef AttachmentThumbnailBuilder = Widget Function(
|
||||||
BuildContext,
|
BuildContext,
|
||||||
_SendingAttachment,
|
Attachment,
|
||||||
);
|
);
|
||||||
|
|
||||||
enum ActionsLocation {
|
enum ActionsLocation {
|
||||||
@@ -44,7 +44,7 @@ enum DefaultAttachmentTypes {
|
|||||||
|
|
||||||
const _kMinMediaPickerSize = 360.0;
|
const _kMinMediaPickerSize = 360.0;
|
||||||
|
|
||||||
const _kMaxAttachmentSize = 20480; //20MB
|
const _kMaxAttachmentSize = 20971520; // 20MB in Bytes
|
||||||
|
|
||||||
/// Inactive state
|
/// Inactive state
|
||||||
/// 
|
/// 
|
||||||
@@ -99,7 +99,6 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.maxHeight = 150,
|
this.maxHeight = 150,
|
||||||
this.keyboardType = TextInputType.multiline,
|
this.keyboardType = TextInputType.multiline,
|
||||||
this.disableAttachments = false,
|
this.disableAttachments = false,
|
||||||
this.attachmentUploader,
|
|
||||||
this.initialMessage,
|
this.initialMessage,
|
||||||
this.textEditingController,
|
this.textEditingController,
|
||||||
this.actions,
|
this.actions,
|
||||||
@@ -135,9 +134,6 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// If true the attachments button will not be displayed
|
/// If true the attachments button will not be displayed
|
||||||
final bool disableAttachments;
|
final bool disableAttachments;
|
||||||
|
|
||||||
/// A delegate to upload attachments
|
|
||||||
final AttachmentUploader attachmentUploader;
|
|
||||||
|
|
||||||
/// The text controller of the TextField
|
/// The text controller of the TextField
|
||||||
final TextEditingController textEditingController;
|
final TextEditingController textEditingController;
|
||||||
|
|
||||||
@@ -178,7 +174,7 @@ class MessageInput extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MessageInputState extends State<MessageInput> {
|
class MessageInputState extends State<MessageInput> {
|
||||||
final _attachments = <String, _SendingAttachment>{};
|
final _attachments = <String, Attachment>{};
|
||||||
final List<User> _mentionedUsers = [];
|
final List<User> _mentionedUsers = [];
|
||||||
|
|
||||||
final _imagePicker = ImagePicker();
|
final _imagePicker = ImagePicker();
|
||||||
@@ -204,8 +200,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||||
|
|
||||||
AttachmentUploader _attachmentUploader;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -391,8 +385,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: AnimatedCrossFade(
|
child: AnimatedCrossFade(
|
||||||
crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) &&
|
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
|
||||||
_attachments.values.every((a) => a.isUploaded == true))
|
|
||||||
? CrossFadeState.showFirst
|
? CrossFadeState.showFirst
|
||||||
: CrossFadeState.showSecond,
|
: CrossFadeState.showSecond,
|
||||||
firstChild: _buildSendButton(context),
|
firstChild: _buildSendButton(context),
|
||||||
@@ -786,7 +779,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
Widget _buildFilePickerSection() {
|
Widget _buildFilePickerSection() {
|
||||||
final _attachmentContainsFile = _attachments.values.any((it) {
|
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||||
return it.attachmentType == 'file';
|
return it.type == 'file';
|
||||||
});
|
});
|
||||||
|
|
||||||
Color _getIconColor(int index) {
|
Color _getIconColor(int index) {
|
||||||
@@ -943,7 +936,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
Widget _buildPickerSection() {
|
Widget _buildPickerSection() {
|
||||||
final _attachmentContainsFile = _attachments.values.any((it) {
|
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||||
return it.attachmentType == 'file';
|
return it.type == 'file';
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (_filePickerIndex) {
|
switch (_filePickerIndex) {
|
||||||
@@ -1045,98 +1038,47 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _addAttachment(AssetEntity medium) async {
|
void _addAttachment(AssetEntity medium) async {
|
||||||
final attachmentId = medium.id;
|
final mediaFile = await medium.originFile.timeout(
|
||||||
_attachments[attachmentId] = _SendingAttachment(id: attachmentId);
|
Duration(seconds: 5),
|
||||||
try {
|
onTimeout: () => medium.originFile,
|
||||||
final mediaFile = await medium.originFile.timeout(
|
);
|
||||||
Duration(seconds: 5),
|
|
||||||
onTimeout: () => medium.originFile,
|
|
||||||
);
|
|
||||||
|
|
||||||
var file = PlatformFile(
|
var file = AttachmentFile(
|
||||||
path: mediaFile.path,
|
path: mediaFile.path,
|
||||||
size: ((await mediaFile.length()) / 1024).ceil(),
|
size: await mediaFile.length(),
|
||||||
bytes: mediaFile.readAsBytesSync(),
|
bytes: mediaFile.readAsBytesSync(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (file.size > _kMaxAttachmentSize) {
|
if (file.size > _kMaxAttachmentSize) {
|
||||||
if (medium?.type == AssetType.video) {
|
if (medium?.type == AssetType.video) {
|
||||||
final mediaInfo = await compressVideoService.compress(file.path);
|
final mediaInfo = await VideoService.compressVideo(file.path);
|
||||||
|
|
||||||
if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) {
|
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(attachmentId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
file = PlatformFile(
|
|
||||||
name: file.name,
|
|
||||||
size: (mediaInfo.filesize / 1024).ceil(),
|
|
||||||
bytes: await mediaInfo.file.readAsBytes(),
|
|
||||||
path: mediaInfo.path,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_showErrorAlert(
|
_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;
|
||||||
}
|
}
|
||||||
}
|
file = AttachmentFile(
|
||||||
|
name: file.name,
|
||||||
setState(() {
|
size: mediaInfo.filesize,
|
||||||
_attachments.update(attachmentId, (it) {
|
bytes: await mediaInfo.file.readAsBytes(),
|
||||||
return it.copyWith(
|
path: mediaInfo.path,
|
||||||
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));
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
_attachments.update(attachmentId, (it) {
|
_showErrorAlert(
|
||||||
return it.copyWith(attachment: it.attachment.copyWith(assetUrl: url));
|
'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) {
|
Widget _buildCommandIcon(String iconType) {
|
||||||
@@ -1561,10 +1503,10 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
Widget _buildAttachments() {
|
Widget _buildAttachments() {
|
||||||
if (_attachments.isEmpty) return Offstage();
|
if (_attachments.isEmpty) return Offstage();
|
||||||
final fileAttachments = _attachments.values
|
final fileAttachments = _attachments.values
|
||||||
.where((it) => it.attachmentType == 'file')
|
.where((it) => it.type == 'file')
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
final remainingAttachments = _attachments.values
|
final remainingAttachments = _attachments.values
|
||||||
.where((it) => it.attachmentType != 'file')
|
.where((it) => it.type != 'file')
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -1582,37 +1524,20 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: FileAttachment(
|
child: FileAttachment(
|
||||||
attachment: e.attachment,
|
message: null,
|
||||||
attachmentType: FileAttachmentType.local,
|
attachment: e,
|
||||||
file: e.file,
|
|
||||||
size: Size(
|
size: Size(
|
||||||
MediaQuery.of(context).size.width * 0.65,
|
MediaQuery.of(context).size.width * 0.65,
|
||||||
56.0,
|
56.0,
|
||||||
),
|
),
|
||||||
trailing: Padding(
|
trailing: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: InkWell(
|
child: _buildRemoveButton(e),
|
||||||
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));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.insertBetween(const SizedBox(width: 8)),
|
.insertBetween(const SizedBox(height: 8)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1638,16 +1563,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
child: _buildAttachment(attachment),
|
child: _buildAttachment(attachment),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildRemoveButton(attachment),
|
Positioned(
|
||||||
if (!attachment.isUploaded)
|
top: 8,
|
||||||
Positioned.fill(
|
right: 8,
|
||||||
child: Center(
|
child: _buildRemoveButton(attachment),
|
||||||
child: Padding(
|
),
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1660,44 +1580,10 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUploadProgressIndicator(int uploaded, int total) {
|
Widget _buildRemoveButton(Attachment attachment) {
|
||||||
final theme = StreamChatTheme.of(context);
|
|
||||||
return Container(
|
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,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
top: 4,
|
|
||||||
right: 4,
|
|
||||||
child: RawMaterialButton(
|
child: RawMaterialButton(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
@@ -1721,21 +1607,18 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAttachment(_SendingAttachment attachment) {
|
Widget _buildAttachment(Attachment attachment) {
|
||||||
if (widget.attachmentThumbnailBuilders
|
if (attachment == null) return Offstage();
|
||||||
?.containsKey(attachment.attachmentType) ==
|
|
||||||
|
if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) ==
|
||||||
true) {
|
true) {
|
||||||
return widget.attachmentThumbnailBuilders[attachment.attachmentType](
|
return widget.attachmentThumbnailBuilders[attachment.type](
|
||||||
context,
|
context,
|
||||||
attachment,
|
attachment,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment.attachment == null) {
|
switch (attachment.type) {
|
||||||
return SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (attachment.attachmentType) {
|
|
||||||
case 'image':
|
case 'image':
|
||||||
case 'giphy':
|
case 'giphy':
|
||||||
return attachment.file != null
|
return attachment.file != null
|
||||||
@@ -1749,32 +1632,33 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: Image.network(
|
: CachedNetworkImage(
|
||||||
attachment.attachment.imageUrl,
|
imageUrl: attachment.imageUrl ??
|
||||||
|
attachment.assetUrl ??
|
||||||
|
attachment.thumbUrl,
|
||||||
fit: BoxFit.cover,
|
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':
|
case 'video':
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Container(
|
||||||
child: Container(
|
height: 104,
|
||||||
child: FutureBuilder<File>(
|
width: 104,
|
||||||
future: VideoCompress.getFileThumbnail(attachment.file.path),
|
child: VideoThumbnailImage(
|
||||||
builder: (context, snapshot) {
|
video: attachment.file.path,
|
||||||
if (!snapshot.hasData) {
|
fit: BoxFit.cover,
|
||||||
return Image.asset(
|
|
||||||
'images/placeholder.png',
|
|
||||||
package: 'stream_chat_flutter',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Image.file(
|
|
||||||
snapshot.data,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -1787,7 +1671,6 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
return Container(
|
return Container(
|
||||||
child: Icon(Icons.insert_drive_file),
|
child: Icon(Icons.insert_drive_file),
|
||||||
@@ -1953,11 +1836,9 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
/// Use this to add custom type attachments
|
/// Use this to add custom type attachments
|
||||||
void addAttachment(Attachment attachment) {
|
void addAttachment(Attachment attachment) {
|
||||||
setState(() {
|
setState(() {
|
||||||
final _attachment = _SendingAttachment(
|
_attachments[attachment.id] = attachment.copyWith(
|
||||||
attachment: attachment,
|
uploadState: attachment.uploadState ?? UploadState.success(),
|
||||||
totalUploaded: attachment.extraData['file_size'],
|
|
||||||
);
|
);
|
||||||
_attachments[_attachment.id] = _attachment;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1966,7 +1847,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async {
|
void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async {
|
||||||
setState(() => _inputEnabled = false);
|
setState(() => _inputEnabled = false);
|
||||||
|
|
||||||
PlatformFile file;
|
AttachmentFile file;
|
||||||
String attachmentType;
|
String attachmentType;
|
||||||
|
|
||||||
if (fileType == DefaultAttachmentTypes.image) {
|
if (fileType == DefaultAttachmentTypes.image) {
|
||||||
@@ -1988,8 +1869,8 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final bytes = await pickedFile.readAsBytes();
|
final bytes = await pickedFile.readAsBytes();
|
||||||
file = PlatformFile(
|
file = AttachmentFile(
|
||||||
size: (bytes.length / 1024).ceil(),
|
size: bytes.length,
|
||||||
path: pickedFile.path,
|
path: pickedFile.path,
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
);
|
);
|
||||||
@@ -2007,7 +1888,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
withData: true,
|
withData: true,
|
||||||
);
|
);
|
||||||
if (res?.files?.isNotEmpty == 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;
|
final mimeType = file.path.split('/').last.mimeType;
|
||||||
|
|
||||||
var extraDataMap = <String, dynamic>{};
|
final extraDataMap = <String, dynamic>{};
|
||||||
|
|
||||||
if (camera) {
|
if (camera) {
|
||||||
if (mimeType.type == 'video' || mimeType.type == 'image') {
|
if (mimeType.type == 'video' || mimeType.type == 'image') {
|
||||||
@@ -2035,97 +1916,46 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
extraDataMap['file_size'] = file.size;
|
extraDataMap['file_size'] = file.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
final attachment = _SendingAttachment(
|
final attachment = Attachment(
|
||||||
file: file,
|
file: file,
|
||||||
attachment: Attachment(
|
type: attachmentType,
|
||||||
localUri: file.path != null ? Uri.parse(file.path) : null,
|
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
|
||||||
type: attachmentType,
|
|
||||||
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
|
|
||||||
title: file.name,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
final attachmentId = attachment.id;
|
|
||||||
|
|
||||||
setState(() => _attachments[attachmentId] = attachment);
|
_attachments[attachment.id] = attachment;
|
||||||
|
|
||||||
if (file.size / 1024 > _kMaxAttachmentSize) {
|
if (file.size > _kMaxAttachmentSize) {
|
||||||
if (attachmentType == 'video') {
|
if (attachmentType == 'Video') {
|
||||||
final mediaInfo = await compressVideoService.compress(file.path);
|
final mediaInfo = await VideoService.compressVideo(file.path);
|
||||||
file = PlatformFile(
|
|
||||||
name: mediaInfo.title,
|
if (mediaInfo.filesize > _kMaxAttachmentSize) {
|
||||||
size: (mediaInfo.filesize / 1024).ceil(),
|
_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(),
|
bytes: await mediaInfo.file.readAsBytes(),
|
||||||
path: mediaInfo.path,
|
path: mediaInfo.path,
|
||||||
);
|
);
|
||||||
setState(() {
|
|
||||||
_attachments.update(attachmentId, (it) => it.copyWith(file: file));
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
_showErrorAlert(
|
_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.',
|
||||||
);
|
);
|
||||||
setState(() => _attachments.remove(attachmentId));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
setState(() {
|
||||||
final url = await _uploadAttachment(
|
_attachments.update(attachment.id, (it) {
|
||||||
file,
|
return it.copyWith(
|
||||||
fileType,
|
file: file,
|
||||||
onSendProgress: (sent, total) {
|
extraData: {...it.extraData}..update('file_size', (_) => file.size),
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildIdleSendButton(BuildContext context) {
|
Widget _buildIdleSendButton(BuildContext context) {
|
||||||
@@ -2203,7 +2033,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
if (widget.editMessage != null) {
|
if (widget.editMessage != null) {
|
||||||
message = widget.editMessage.copyWith(
|
message = widget.editMessage.copyWith(
|
||||||
text: text,
|
text: text,
|
||||||
attachments: _getAttachments(attachments).toList(),
|
attachments: attachments,
|
||||||
mentionedUsers:
|
mentionedUsers:
|
||||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
||||||
);
|
);
|
||||||
@@ -2211,7 +2041,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
message = (widget.initialMessage ?? Message()).copyWith(
|
message = (widget.initialMessage ?? Message()).copyWith(
|
||||||
parentId: widget.parentMessage?.id,
|
parentId: widget.parentMessage?.id,
|
||||||
text: text,
|
text: text,
|
||||||
attachments: _getAttachments(attachments).toList(),
|
attachments: attachments,
|
||||||
mentionedUsers:
|
mentionedUsers:
|
||||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
||||||
showInChannel: widget.parentMessage != null ? _sendAsDm : null,
|
showInChannel: widget.parentMessage != null ? _sendAsDm : null,
|
||||||
@@ -2237,13 +2067,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
_mentionedUsers.clear();
|
_mentionedUsers.clear();
|
||||||
|
|
||||||
if (widget.editMessage == null ||
|
if (widget.editMessage == null ||
|
||||||
widget.editMessage.status == MessageSendingStatus.failed) {
|
widget.editMessage.status == MessageSendingStatus.failed ||
|
||||||
|
widget.editMessage.status == MessageSendingStatus.sending) {
|
||||||
sendingFuture = channel.sendMessage(message);
|
sendingFuture = channel.sendMessage(message);
|
||||||
} else {
|
} else {
|
||||||
sendingFuture = StreamChat.of(context).client.updateMessage(
|
sendingFuture = channel.updateMessage(message);
|
||||||
message,
|
|
||||||
channel.cid,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendingFuture.then((resp) {
|
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;
|
StreamSubscription _keyboardListener;
|
||||||
|
|
||||||
void _showErrorAlert(String description) {
|
void _showErrorAlert(String description) {
|
||||||
@@ -2334,16 +2156,12 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
void _parseExistingMessage(Message message) {
|
void _parseExistingMessage(Message message) {
|
||||||
textEditingController.text = message.text;
|
textEditingController.text = message.text;
|
||||||
|
|
||||||
_messageIsPresent = true;
|
_messageIsPresent = true;
|
||||||
|
for (final attachment in message.attachments) {
|
||||||
message.attachments?.forEach((attachment) {
|
_attachments[attachment.id] = attachment.copyWith(
|
||||||
final _attachment = _SendingAttachment(
|
uploadState: attachment.uploadState ?? UploadState.success(),
|
||||||
attachment: attachment,
|
|
||||||
totalUploaded: attachment.extraData['file_size'],
|
|
||||||
);
|
);
|
||||||
_attachments[_attachment.id] = _attachment;
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2363,63 +2181,10 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
FocusScope.of(context).requestFocus(_focusNode);
|
FocusScope.of(context).requestFocus(_focusNode);
|
||||||
_initialized = true;
|
_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();
|
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.
|
/// Represents a 2-tuple, or pair.
|
||||||
class Tuple2<T1, T2> {
|
class Tuple2<T1, T2> {
|
||||||
/// Returns the first item of the tuple
|
/// Returns the first item of the tuple
|
||||||
|
|||||||
@@ -239,46 +239,50 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
final MessageListController _messageListController = MessageListController();
|
final MessageListController _messageListController = MessageListController();
|
||||||
|
|
||||||
final Map<String, VideoPackage> videoPackages = {};
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MessageListCore(
|
return WillPopScope(
|
||||||
loadingBuilder: (context) {
|
onWillPop: () async {
|
||||||
return Center(
|
print('Getting popped');
|
||||||
child: const CircularProgressIndicator(),
|
return false;
|
||||||
);
|
|
||||||
},
|
|
||||||
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)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
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;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoPackages: videoPackages,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -939,10 +942,12 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoPackages: videoPackages,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!message.isDeleted && !message.isSystem && !message.isEphemeral) {
|
if (!message.isDeleted &&
|
||||||
|
!message.isSystem &&
|
||||||
|
!message.isEphemeral &&
|
||||||
|
widget.onMessageSwiped != null) {
|
||||||
child = Swipeable(
|
child = Swipeable(
|
||||||
onSwipeEnd: () {
|
onSwipeEnd: () {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
@@ -1056,7 +1061,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
streamChannel.reloadChannel();
|
streamChannel.reloadChannel();
|
||||||
}
|
}
|
||||||
_messageNewListener?.cancel();
|
_messageNewListener?.cancel();
|
||||||
videoPackages.values.forEach((e) => e.dispose());
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
final ShapeBorder messageShape;
|
final ShapeBorder messageShape;
|
||||||
final ShapeBorder attachmentShape;
|
final ShapeBorder attachmentShape;
|
||||||
final void Function(User) onUserAvatarTap;
|
final void Function(User) onUserAvatarTap;
|
||||||
final Map<String, VideoPackage> videoPackages;
|
|
||||||
|
|
||||||
const MessageReactionsModal({
|
const MessageReactionsModal({
|
||||||
Key key,
|
Key key,
|
||||||
@@ -37,7 +36,6 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
this.showUserAvatar = DisplayWidget.show,
|
this.showUserAvatar = DisplayWidget.show,
|
||||||
this.onUserAvatarTap,
|
this.onUserAvatarTap,
|
||||||
this.videoPackages,
|
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -146,7 +144,6 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
(message.status ==
|
(message.status ==
|
||||||
MessageSendingStatus.sent ||
|
MessageSendingStatus.sent ||
|
||||||
message.status == null),
|
message.status == null),
|
||||||
videoPackages: videoPackages,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (message.latestReactions?.isNotEmpty == true) ...[
|
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/src/url_attachment.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'attachment/attachment.dart';
|
||||||
import 'extension.dart';
|
import 'extension.dart';
|
||||||
import 'image_group.dart';
|
import 'image_group.dart';
|
||||||
import 'message_text.dart';
|
import 'message_text.dart';
|
||||||
@@ -142,9 +143,6 @@ class MessageWidget extends StatefulWidget {
|
|||||||
/// Function called when quotedMessage is tapped
|
/// Function called when quotedMessage is tapped
|
||||||
final OnQuotedMessageTap onQuotedMessageTap;
|
final OnQuotedMessageTap onQuotedMessageTap;
|
||||||
|
|
||||||
/// The cache for the video controllers of attachments IDed as message ID + attachment index
|
|
||||||
final Map<String, VideoPackage> videoPackages;
|
|
||||||
|
|
||||||
///
|
///
|
||||||
MessageWidget({
|
MessageWidget({
|
||||||
Key key,
|
Key key,
|
||||||
@@ -193,7 +191,6 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.attachmentPadding = EdgeInsets.zero,
|
this.attachmentPadding = EdgeInsets.zero,
|
||||||
this.allRead = false,
|
this.allRead = false,
|
||||||
this.onQuotedMessageTap,
|
this.onQuotedMessageTap,
|
||||||
this.videoPackages,
|
|
||||||
}) : attachmentBuilders = {
|
}) : attachmentBuilders = {
|
||||||
'image': (context, message, attachment) {
|
'image': (context, message, attachment) {
|
||||||
return ImageAttachment(
|
return ImageAttachment(
|
||||||
@@ -236,6 +233,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
},
|
},
|
||||||
'file': (context, message, attachment) {
|
'file': (context, message, attachment) {
|
||||||
return FileAttachment(
|
return FileAttachment(
|
||||||
|
message: message,
|
||||||
attachment: attachment,
|
attachment: attachment,
|
||||||
size: Size(
|
size: Size(
|
||||||
MediaQuery.of(context).size.width * 0.8,
|
MediaQuery.of(context).size.width * 0.8,
|
||||||
@@ -250,7 +248,8 @@ class MessageWidget extends StatefulWidget {
|
|||||||
_MessageWidgetState createState() => _MessageWidgetState();
|
_MessageWidgetState createState() => _MessageWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MessageWidgetState extends State<MessageWidget> {
|
class _MessageWidgetState extends State<MessageWidget>
|
||||||
|
with AutomaticKeepAliveClientMixin<MessageWidget> {
|
||||||
bool get showThreadReplyIndicator => widget.showThreadReplyIndicator;
|
bool get showThreadReplyIndicator => widget.showThreadReplyIndicator;
|
||||||
|
|
||||||
bool get showSendingIndicator => widget.showSendingIndicator;
|
bool get showSendingIndicator => widget.showSendingIndicator;
|
||||||
@@ -298,8 +297,12 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
showSendingIndicator ||
|
showSendingIndicator ||
|
||||||
isDeleted;
|
isDeleted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => widget.message.attachments?.isNotEmpty == true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
final avatarWidth = widget.messageTheme.avatarTheme.constraints.maxWidth;
|
final avatarWidth = widget.messageTheme.avatarTheme.constraints.maxWidth;
|
||||||
var leftPadding =
|
var leftPadding =
|
||||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
||||||
@@ -744,7 +747,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
!isFailedState &&
|
!isFailedState &&
|
||||||
widget.onThreadTap != null,
|
widget.onThreadTap != null,
|
||||||
showFlagButton: widget.showFlagButton,
|
showFlagButton: widget.showFlagButton,
|
||||||
videoPackages: widget.videoPackages,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -773,7 +775,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||||
onThreadTap: widget.onThreadTap,
|
onThreadTap: widget.onThreadTap,
|
||||||
showReactions: widget.showReactions,
|
showReactions: widget.showReactions,
|
||||||
videoPackages: widget.videoPackages,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -824,6 +825,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
),
|
),
|
||||||
images: images,
|
images: images,
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
|
messageTheme: widget.messageTheme,
|
||||||
onShowMessage: widget.onShowMessage,
|
onShowMessage: widget.onShowMessage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -838,41 +840,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
children: widget.message.attachments
|
children: widget.message.attachments
|
||||||
?.where((element) => element.ogScrapeUrl == null)
|
?.where((element) => element.ogScrapeUrl == null)
|
||||||
?.map((attachment) {
|
?.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 =
|
final attachmentBuilder =
|
||||||
widget.attachmentBuilders[attachment.type];
|
widget.attachmentBuilders[attachment.type];
|
||||||
|
|
||||||
@@ -885,7 +852,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
return wrapAttachmentWidget(
|
return wrapAttachmentWidget(
|
||||||
context,
|
context,
|
||||||
attachmentWidget,
|
attachmentWidget,
|
||||||
attachment: attachment,
|
|
||||||
);
|
);
|
||||||
})?.insertBetween(SizedBox(
|
})?.insertBetween(SizedBox(
|
||||||
height: widget.attachmentPadding.vertical / 2,
|
height: widget.attachmentPadding.vertical / 2,
|
||||||
@@ -897,9 +863,8 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
|
|
||||||
Widget wrapAttachmentWidget(
|
Widget wrapAttachmentWidget(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Widget attachmentWidget, {
|
Widget attachmentWidget,
|
||||||
Attachment attachment,
|
) {
|
||||||
}) {
|
|
||||||
final attachmentShape =
|
final attachmentShape =
|
||||||
widget.attachmentShape ?? _getDefaultAttachmentShape(context);
|
widget.attachmentShape ?? _getDefaultAttachmentShape(context);
|
||||||
return Material(
|
return Material(
|
||||||
@@ -930,8 +895,29 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
|
|
||||||
Widget _buildSendingIndicator() {
|
Widget _buildSendingIndicator() {
|
||||||
final style = widget.messageTheme.createdAt;
|
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(
|
Widget child = SendingIndicator(
|
||||||
message: widget.message,
|
message: message,
|
||||||
isMessageRead: isMessageRead,
|
isMessageRead: isMessageRead,
|
||||||
size: style.fontSize,
|
size: style.fontSize,
|
||||||
);
|
);
|
||||||
@@ -1032,18 +1018,12 @@ class _MessageWidgetState extends State<MessageWidget> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (widget.message.status == MessageSendingStatus.failed_update) {
|
if (widget.message.status == MessageSendingStatus.failed_update) {
|
||||||
StreamChat.of(context).client.updateMessage(
|
channel.updateMessage(widget.message);
|
||||||
widget.message,
|
|
||||||
channel.cid,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.message.status == MessageSendingStatus.failed_delete) {
|
if (widget.message.status == MessageSendingStatus.failed_delete) {
|
||||||
StreamChat.of(context).client.deleteMessage(
|
channel.deleteMessage(widget.message);
|
||||||
widget.message,
|
|
||||||
channel.cid,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
import 'attachment_error.dart';
|
import 'attachment/attachment.dart';
|
||||||
import 'extension.dart';
|
import 'extension.dart';
|
||||||
import 'image_attachment.dart';
|
|
||||||
import 'message_text.dart';
|
import 'message_text.dart';
|
||||||
import 'stream_chat_theme.dart';
|
import 'stream_chat_theme.dart';
|
||||||
import 'user_avatar.dart';
|
import 'user_avatar.dart';
|
||||||
@@ -200,10 +199,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return AttachmentError(
|
return AttachmentError(size: size);
|
||||||
attachment: attachment,
|
|
||||||
size: size,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _parseAttachments(BuildContext context) {
|
Widget _parseAttachments(BuildContext context) {
|
||||||
@@ -231,8 +227,8 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Material(
|
child: Material(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.antiAlias,
|
||||||
color: Colors.transparent,
|
type: MaterialType.transparency,
|
||||||
shape: attachment.type == 'file' ? null : _getDefaultShape(context),
|
shape: attachment.type == 'file' ? null : _getDefaultShape(context),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
@@ -294,10 +290,9 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
imageUrl:
|
imageUrl:
|
||||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
|
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
|
||||||
errorWidget: (context, url, error) => AttachmentError(
|
errorWidget: (context, url, error) {
|
||||||
attachment: attachment,
|
return AttachmentError(size: size);
|
||||||
size: size,
|
},
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -901,4 +901,16 @@ class StreamSvgIcon extends StatelessWidget {
|
|||||||
height: size,
|
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_chat_flutter.dart';
|
||||||
import 'stream_svg_icon.dart';
|
import 'stream_svg_icon.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
Future<void> launchURL(BuildContext context, String url) async {
|
Future<void> launchURL(BuildContext context, String url) async {
|
||||||
if (await canLaunch(url)) {
|
if (await canLaunch(url)) {
|
||||||
@@ -215,7 +216,7 @@ String getWebsiteName(String hostName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A method returns a human readable string representing a file _size
|
/// 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';
|
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/channel_preview.dart';
|
||||||
export 'src/date_divider.dart';
|
export 'src/date_divider.dart';
|
||||||
export 'src/deleted_message.dart';
|
export 'src/deleted_message.dart';
|
||||||
export 'src/file_attachment.dart';
|
export 'src/attachment/attachment.dart';
|
||||||
export 'src/full_screen_media.dart';
|
export 'src/full_screen_media.dart';
|
||||||
export 'src/image_header.dart';
|
export 'src/image_header.dart';
|
||||||
export 'src/image_footer.dart';
|
export 'src/image_footer.dart';
|
||||||
export 'src/giphy_attachment.dart';
|
|
||||||
export 'src/image_attachment.dart';
|
|
||||||
export 'src/message_input.dart';
|
export 'src/message_input.dart';
|
||||||
export 'src/message_list_view.dart';
|
export 'src/message_list_view.dart';
|
||||||
export 'src/message_text.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/user_list_view.dart';
|
export 'src/user_list_view.dart';
|
||||||
export 'src/utils.dart';
|
export 'src/utils.dart';
|
||||||
export 'src/video_attachment.dart';
|
|
||||||
export 'src/message_search_item.dart';
|
export 'src/message_search_item.dart';
|
||||||
export 'src/message_search_list_view.dart';
|
export 'src/message_search_list_view.dart';
|
||||||
export 'src/unread_indicator.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
|
characters: ^1.0.0
|
||||||
dio: ^3.0.10
|
dio: ^3.0.10
|
||||||
path_provider: ^1.6.27
|
path_provider: ^1.6.27
|
||||||
|
video_thumbnail: ^0.2.5+1
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
assets:
|
assets:
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
cupertino_icons: ^1.0.0
|
cupertino_icons: ^1.0.0
|
||||||
stream_chat: ^1.0.0-beta
|
stream_chat:
|
||||||
|
path: ../../stream_chat
|
||||||
stream_chat_persistence:
|
stream_chat_persistence:
|
||||||
path: ../
|
path: ../
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ extension MessageEntityX on MessageEntity {
|
|||||||
ownReactions: ownReactions,
|
ownReactions: ownReactions,
|
||||||
attachments: attachments?.map((it) {
|
attachments: attachments?.map((it) {
|
||||||
final json = jsonDecode(it);
|
final json = jsonDecode(it);
|
||||||
return Attachment.fromJson(json);
|
return Attachment.fromData(json);
|
||||||
})?.toList(),
|
})?.toList(),
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
extraData: extraData,
|
extraData: extraData,
|
||||||
@@ -47,7 +47,9 @@ extension MessageX on Message {
|
|||||||
MessageEntity toEntity({String cid}) {
|
MessageEntity toEntity({String cid}) {
|
||||||
return MessageEntity(
|
return MessageEntity(
|
||||||
id: id,
|
id: id,
|
||||||
attachments: attachments?.map((it) => jsonEncode(it))?.toList() ?? [],
|
attachments: attachments?.map((it) {
|
||||||
|
return jsonEncode(it.toData());
|
||||||
|
})?.toList(),
|
||||||
channelCid: cid,
|
channelCid: cid,
|
||||||
type: type,
|
type: type,
|
||||||
parentId: parentId,
|
parentId: parentId,
|
||||||
|
|||||||
Reference in New Issue
Block a user