Merge branch 'client-enhance-implement-thumbnails' into 'master'
[Client] Implement thumbnails See merge request famedly/famedlysdk!261
This commit is contained in:
commit
f2d4a39fc0
|
@ -488,6 +488,7 @@ class Client {
|
||||||
|
|
||||||
/// Uploads a new user avatar for this user.
|
/// Uploads a new user avatar for this user.
|
||||||
Future<void> setAvatar(MatrixFile file) async {
|
Future<void> setAvatar(MatrixFile file) async {
|
||||||
|
await file.resize(width: 128);
|
||||||
final uploadResp = await upload(file);
|
final uploadResp = await upload(file);
|
||||||
await jsonRequest(
|
await jsonRequest(
|
||||||
type: HTTPType.PUT,
|
type: HTTPType.PUT,
|
||||||
|
|
|
@ -429,31 +429,50 @@ class Event {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get hasThumbnail =>
|
||||||
|
content['info'] is Map<String, dynamic> &&
|
||||||
|
(content['info'].containsKey('thumbnail_url') ||
|
||||||
|
content['info'].containsKey('thumbnail_file'));
|
||||||
|
|
||||||
/// Downloads (and decryptes if necessary) the attachment of this
|
/// Downloads (and decryptes if necessary) the attachment of this
|
||||||
/// event and returns it as a [MatrixFile]. If this event doesn't
|
/// event and returns it as a [MatrixFile]. If this event doesn't
|
||||||
/// contain an attachment, this throws an error.
|
/// contain an attachment, this throws an error. Set [getThumbnail] to
|
||||||
Future<MatrixFile> downloadAndDecryptAttachment() async {
|
/// true to download the thumbnail instead.
|
||||||
|
Future<MatrixFile> downloadAndDecryptAttachment(
|
||||||
|
{bool getThumbnail = false}) async {
|
||||||
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
|
if (![EventTypes.Message, EventTypes.Sticker].contains(type)) {
|
||||||
throw ("This event has the type '$typeKey' and so it can't contain an attachment.");
|
throw ("This event has the type '$typeKey' and so it can't contain an attachment.");
|
||||||
}
|
}
|
||||||
if (!content.containsKey('url') && !content.containsKey('file')) {
|
if (!getThumbnail &&
|
||||||
|
!content.containsKey('url') &&
|
||||||
|
!content.containsKey('file')) {
|
||||||
throw ("This event hasn't any attachment.");
|
throw ("This event hasn't any attachment.");
|
||||||
}
|
}
|
||||||
final isEncrypted = !content.containsKey('url');
|
if (getThumbnail && !hasThumbnail) {
|
||||||
|
throw ("This event hasn't any thumbnail.");
|
||||||
|
}
|
||||||
|
final isEncrypted = getThumbnail
|
||||||
|
? !content['info'].containsKey('thumbnail_url')
|
||||||
|
: !content.containsKey('url');
|
||||||
|
|
||||||
if (isEncrypted && !room.client.encryptionEnabled) {
|
if (isEncrypted && !room.client.encryptionEnabled) {
|
||||||
throw ('Encryption is not enabled in your Client.');
|
throw ('Encryption is not enabled in your Client.');
|
||||||
}
|
}
|
||||||
var mxContent =
|
var mxContent = getThumbnail
|
||||||
MxContent(isEncrypted ? content['file']['url'] : content['url']);
|
? MxContent(isEncrypted
|
||||||
|
? content['info']['thumbnail_file']['url']
|
||||||
|
: content['info']['thumbnail_url'])
|
||||||
|
: MxContent(isEncrypted ? content['file']['url'] : content['url']);
|
||||||
|
|
||||||
Uint8List uint8list;
|
Uint8List uint8list;
|
||||||
|
|
||||||
// Is this file storeable?
|
// Is this file storeable?
|
||||||
|
final infoMap =
|
||||||
|
getThumbnail ? content['info']['thumbnail_info'] : content['info'];
|
||||||
final storeable = room.client.storeAPI.extended &&
|
final storeable = room.client.storeAPI.extended &&
|
||||||
content['info'] is Map<String, dynamic> &&
|
infoMap is Map<String, dynamic> &&
|
||||||
content['info']['size'] is int &&
|
infoMap['size'] is int &&
|
||||||
content['info']['size'] <= ExtendedStoreAPI.MAX_FILE_SIZE;
|
infoMap['size'] <= ExtendedStoreAPI.MAX_FILE_SIZE;
|
||||||
|
|
||||||
if (storeable) {
|
if (storeable) {
|
||||||
uint8list = await room.client.store.getFile(mxContent.mxc);
|
uint8list = await room.client.store.getFile(mxContent.mxc);
|
||||||
|
@ -470,15 +489,16 @@ class Event {
|
||||||
|
|
||||||
// Decrypt the file
|
// Decrypt the file
|
||||||
if (isEncrypted) {
|
if (isEncrypted) {
|
||||||
if (!content.containsKey('file') ||
|
final fileMap =
|
||||||
!content['file']['key']['key_ops'].contains('decrypt')) {
|
getThumbnail ? content['info']['thumbnail_file'] : content['file'];
|
||||||
|
if (!fileMap['key']['key_ops'].contains('decrypt')) {
|
||||||
throw ("Missing 'decrypt' in 'key_ops'.");
|
throw ("Missing 'decrypt' in 'key_ops'.");
|
||||||
}
|
}
|
||||||
final encryptedFile = EncryptedFile();
|
final encryptedFile = EncryptedFile();
|
||||||
encryptedFile.data = uint8list;
|
encryptedFile.data = uint8list;
|
||||||
encryptedFile.iv = content['file']['iv'];
|
encryptedFile.iv = fileMap['iv'];
|
||||||
encryptedFile.k = content['file']['key']['k'];
|
encryptedFile.k = fileMap['key']['k'];
|
||||||
encryptedFile.sha256 = content['file']['hashes']['sha256'];
|
encryptedFile.sha256 = fileMap['hashes']['sha256'];
|
||||||
uint8list = await decryptFile(encryptedFile);
|
uint8list = await decryptFile(encryptedFile);
|
||||||
}
|
}
|
||||||
return MatrixFile(bytes: uint8list, path: '/$body');
|
return MatrixFile(bytes: uint8list, path: '/$body');
|
||||||
|
|
|
@ -34,6 +34,7 @@ import 'package:famedlysdk/src/utils/matrix_exception.dart';
|
||||||
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
import 'package:famedlysdk/src/utils/matrix_file.dart';
|
||||||
import 'package:famedlysdk/src/utils/mx_content.dart';
|
import 'package:famedlysdk/src/utils/mx_content.dart';
|
||||||
import 'package:famedlysdk/src/utils/session_key.dart';
|
import 'package:famedlysdk/src/utils/session_key.dart';
|
||||||
|
import 'package:image/image.dart';
|
||||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||||
import 'package:mime_type/mime_type.dart';
|
import 'package:mime_type/mime_type.dart';
|
||||||
import 'package:olm/olm.dart' as olm;
|
import 'package:olm/olm.dart' as olm;
|
||||||
|
@ -478,17 +479,13 @@ class Room {
|
||||||
Map<String, dynamic> info,
|
Map<String, dynamic> info,
|
||||||
bool waitUntilSent = false,
|
bool waitUntilSent = false,
|
||||||
}) async {
|
}) async {
|
||||||
var fileName = file.path.split('/').last;
|
Image fileImage;
|
||||||
final sendEncrypted = encrypted && client.fileEncryptionEnabled;
|
Image thumbnailImage;
|
||||||
EncryptedFile encryptedFile;
|
MatrixFile thumbnail;
|
||||||
if (sendEncrypted) {
|
EncryptedFile encryptedThumbnail;
|
||||||
encryptedFile = await file.encrypt();
|
String thumbnailUploadResp;
|
||||||
}
|
|
||||||
final uploadResp = await client.upload(
|
|
||||||
file,
|
|
||||||
contentType: sendEncrypted ? 'application/octet-stream' : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
var fileName = file.path.split('/').last;
|
||||||
final mimeType = mime(file.path) ?? '';
|
final mimeType = mime(file.path) ?? '';
|
||||||
if (msgType == null) {
|
if (msgType == null) {
|
||||||
final metaType = (mimeType).split('/')[0];
|
final metaType = (mimeType).split('/')[0];
|
||||||
|
@ -504,6 +501,35 @@ class Room {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msgType == 'm.image') {
|
||||||
|
var thumbnailPathParts = file.path.split('/');
|
||||||
|
thumbnailPathParts.last = 'thumbnail_' + thumbnailPathParts.last;
|
||||||
|
final thumbnailPath = thumbnailPathParts.join('/');
|
||||||
|
thumbnail = MatrixFile(bytes: file.bytes, path: thumbnailPath);
|
||||||
|
await thumbnail.resize(width: 512);
|
||||||
|
fileImage = decodeImage(file.bytes.toList());
|
||||||
|
thumbnailImage = decodeImage(thumbnail.bytes.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
final sendEncrypted = encrypted && client.fileEncryptionEnabled;
|
||||||
|
EncryptedFile encryptedFile;
|
||||||
|
if (sendEncrypted) {
|
||||||
|
encryptedFile = await file.encrypt();
|
||||||
|
if (thumbnail != null) {
|
||||||
|
encryptedThumbnail = await thumbnail.encrypt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final uploadResp = await client.upload(
|
||||||
|
file,
|
||||||
|
contentType: sendEncrypted ? 'application/octet-stream' : null,
|
||||||
|
);
|
||||||
|
if (thumbnail != null) {
|
||||||
|
thumbnailUploadResp = await client.upload(
|
||||||
|
thumbnail,
|
||||||
|
contentType: sendEncrypted ? 'application/octet-stream' : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Send event
|
// Send event
|
||||||
var content = <String, dynamic>{
|
var content = <String, dynamic>{
|
||||||
'msgtype': msgType,
|
'msgtype': msgType,
|
||||||
|
@ -529,6 +555,32 @@ class Room {
|
||||||
{
|
{
|
||||||
'mimetype': mimeType,
|
'mimetype': mimeType,
|
||||||
'size': file.size,
|
'size': file.size,
|
||||||
|
if (fileImage != null) 'h': fileImage.height,
|
||||||
|
if (fileImage != null) 'w': fileImage.width,
|
||||||
|
if (thumbnailUploadResp != null && !sendEncrypted)
|
||||||
|
'thumbnail_url': thumbnailUploadResp,
|
||||||
|
if (thumbnailUploadResp != null && sendEncrypted)
|
||||||
|
'thumbnail_file': {
|
||||||
|
'url': thumbnailUploadResp,
|
||||||
|
'mimetype': mimeType,
|
||||||
|
'v': 'v2',
|
||||||
|
'key': {
|
||||||
|
'alg': 'A256CTR',
|
||||||
|
'ext': true,
|
||||||
|
'k': encryptedThumbnail.k,
|
||||||
|
'key_ops': ['encrypt', 'decrypt'],
|
||||||
|
'kty': 'oct'
|
||||||
|
},
|
||||||
|
'iv': encryptedThumbnail.iv,
|
||||||
|
'hashes': {'sha256': encryptedThumbnail.sha256}
|
||||||
|
},
|
||||||
|
if (thumbnailImage != null)
|
||||||
|
'thumbnail_info': {
|
||||||
|
'h': thumbnailImage.height,
|
||||||
|
'mimetype': mimeType,
|
||||||
|
'size': thumbnailImage.length,
|
||||||
|
'w': thumbnailImage.width,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
final sendResponse = sendEvent(
|
final sendResponse = sendEvent(
|
||||||
|
@ -1198,6 +1250,7 @@ class Room {
|
||||||
/// Uploads a new user avatar for this room. Returns the event ID of the new
|
/// Uploads a new user avatar for this room. Returns the event ID of the new
|
||||||
/// m.room.avatar event.
|
/// m.room.avatar event.
|
||||||
Future<String> setAvatar(MatrixFile file) async {
|
Future<String> setAvatar(MatrixFile file) async {
|
||||||
|
await file.resize(width: 256);
|
||||||
final uploadResp = await client.upload(file);
|
final uploadResp = await client.upload(file);
|
||||||
final setAvatarResp = await client.jsonRequest(
|
final setAvatarResp = await client.jsonRequest(
|
||||||
type: HTTPType.PUT,
|
type: HTTPType.PUT,
|
||||||
|
|
|
@ -2,12 +2,31 @@
|
||||||
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:image/image.dart';
|
||||||
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
import 'package:matrix_file_e2ee/matrix_file_e2ee.dart';
|
||||||
|
|
||||||
class MatrixFile {
|
class MatrixFile {
|
||||||
Uint8List bytes;
|
Uint8List bytes;
|
||||||
String path;
|
String path;
|
||||||
|
|
||||||
|
/// If this file is an Image, this will resize it to the
|
||||||
|
/// given width and height. Otherwise returns false.
|
||||||
|
/// At least width or height must be set!
|
||||||
|
Future<bool> resize({int width, int height}) async {
|
||||||
|
if (width == null && height == null) {
|
||||||
|
throw ('At least width or height must be set!');
|
||||||
|
}
|
||||||
|
Image image;
|
||||||
|
try {
|
||||||
|
image = decodeImage(bytes);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final resizedImage = copyResize(image, width: width, height: height);
|
||||||
|
bytes = encodePng(resizedImage);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// Encrypts this file, changes the [bytes] and returns the
|
/// Encrypts this file, changes the [bytes] and returns the
|
||||||
/// encryption information as an [EncryptedFile].
|
/// encryption information as an [EncryptedFile].
|
||||||
Future<EncryptedFile> encrypt() async {
|
Future<EncryptedFile> encrypt() async {
|
||||||
|
|
28
pubspec.lock
28
pubspec.lock
|
@ -8,6 +8,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.36.3"
|
version: "0.36.3"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.13"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -211,6 +218,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
|
image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.12"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -334,6 +348,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
pointycastle:
|
pointycastle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -516,6 +537,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.13"
|
version: "1.0.13"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.7.0"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -11,6 +11,7 @@ dependencies:
|
||||||
http: ^0.12.0+4
|
http: ^0.12.0+4
|
||||||
mime_type: ^0.2.4
|
mime_type: ^0.2.4
|
||||||
canonical_json: ^1.0.0
|
canonical_json: ^1.0.0
|
||||||
|
image: ^2.1.4
|
||||||
|
|
||||||
olm:
|
olm:
|
||||||
git:
|
git:
|
||||||
|
|
Loading…
Reference in a new issue