Merge branch 'braid/compute-implementation' into 'main'
chore: introduce native implementations See merge request famedly/company/frontend/famedlysdk!1087
This commit is contained in:
commit
368162c76b
|
|
@ -34,6 +34,7 @@ const megolmKey = EventTypes.MegolmBackup;
|
|||
|
||||
class KeyManager {
|
||||
final Encryption encryption;
|
||||
|
||||
Client get client => encryption.client;
|
||||
final outgoingShareRequests = <String, KeyManagerKeyShareRequest>{};
|
||||
final incomingShareRequests = <String, KeyManagerKeyShareRequest>{};
|
||||
|
|
@ -568,6 +569,7 @@ class KeyManager {
|
|||
|
||||
GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache;
|
||||
DateTime? _roomKeysVersionCacheDate;
|
||||
|
||||
Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo(
|
||||
[bool useCache = true]) async {
|
||||
if (_roomKeysVersionCache != null &&
|
||||
|
|
@ -715,6 +717,7 @@ class KeyManager {
|
|||
|
||||
bool _isUploadingKeys = false;
|
||||
bool _haveKeysToUpload = true;
|
||||
|
||||
Future<void> backgroundTasks() async {
|
||||
final database = client.database;
|
||||
final userID = client.userID;
|
||||
|
|
@ -745,7 +748,7 @@ class KeyManager {
|
|||
info.authData['public_key'] != backupPubKey) {
|
||||
return;
|
||||
}
|
||||
final args = _GenerateUploadKeysArgs(
|
||||
final args = GenerateUploadKeysArgs(
|
||||
pubkey: backupPubKey,
|
||||
dbSessions: <_DbInboundGroupSessionBundle>[],
|
||||
userId: userID,
|
||||
|
|
@ -768,8 +771,7 @@ class KeyManager {
|
|||
}
|
||||
}
|
||||
final roomKeys =
|
||||
await client.runInBackground<RoomKeys, _GenerateUploadKeysArgs>(
|
||||
_generateUploadKeys, args);
|
||||
await client.nativeImplementations.generateUploadKeys(args);
|
||||
Logs().i('[Key Manager] Uploading ${dbSessions.length} room keys...');
|
||||
// upload the payload...
|
||||
await client.putRoomKeys(info.version, roomKeys);
|
||||
|
|
@ -988,6 +990,7 @@ class KeyManagerKeyShareRequest {
|
|||
class RoomKeyRequest extends ToDeviceEvent {
|
||||
KeyManager keyManager;
|
||||
KeyManagerKeyShareRequest request;
|
||||
|
||||
RoomKeyRequest.fromToDeviceEvent(
|
||||
ToDeviceEvent toDeviceEvent, this.keyManager, this.request)
|
||||
: super(
|
||||
|
|
@ -1035,7 +1038,9 @@ class RoomKeyRequest extends ToDeviceEvent {
|
|||
}
|
||||
}
|
||||
|
||||
RoomKeys _generateUploadKeys(_GenerateUploadKeysArgs args) {
|
||||
/// you would likely want to use [NativeImplementations] and
|
||||
/// [Client.nativeImplementations] instead
|
||||
RoomKeys generateUploadKeysImplementation(GenerateUploadKeysArgs args) {
|
||||
final enc = olm.PkEncryption();
|
||||
try {
|
||||
enc.set_recipient_key(args.pubkey);
|
||||
|
|
@ -1087,14 +1092,40 @@ class _DbInboundGroupSessionBundle {
|
|||
_DbInboundGroupSessionBundle(
|
||||
{required this.dbSession, required this.verified});
|
||||
|
||||
factory _DbInboundGroupSessionBundle.fromJson(Map<dynamic, dynamic> json) =>
|
||||
_DbInboundGroupSessionBundle(
|
||||
dbSession:
|
||||
StoredInboundGroupSession.fromJson(Map.from(json['dbSession'])),
|
||||
verified: json['verified'],
|
||||
);
|
||||
|
||||
Map<String, Object> toJson() => {
|
||||
'dbSession': dbSession.toJson(),
|
||||
'verified': verified,
|
||||
};
|
||||
StoredInboundGroupSession dbSession;
|
||||
bool verified;
|
||||
}
|
||||
|
||||
class _GenerateUploadKeysArgs {
|
||||
_GenerateUploadKeysArgs(
|
||||
class GenerateUploadKeysArgs {
|
||||
GenerateUploadKeysArgs(
|
||||
{required this.pubkey, required this.dbSessions, required this.userId});
|
||||
|
||||
factory GenerateUploadKeysArgs.fromJson(Map<dynamic, dynamic> json) =>
|
||||
GenerateUploadKeysArgs(
|
||||
pubkey: json['pubkey'],
|
||||
dbSessions: (json['dbSessions'] as Iterable)
|
||||
.map((e) => _DbInboundGroupSessionBundle.fromJson(e))
|
||||
.toList(),
|
||||
userId: json['userId'],
|
||||
);
|
||||
|
||||
Map<String, Object> toJson() => {
|
||||
'pubkey': pubkey,
|
||||
'dbSessions': dbSessions.map((e) => e.toJson()).toList(),
|
||||
'userId': userId,
|
||||
};
|
||||
|
||||
String pubkey;
|
||||
List<_DbInboundGroupSessionBundle> dbSessions;
|
||||
String userId;
|
||||
|
|
|
|||
|
|
@ -215,15 +215,14 @@ class SSSS {
|
|||
algorithm: AlgorithmTypes.pbkdf2,
|
||||
bits: ssssKeyLength * 8,
|
||||
);
|
||||
privateKey = await client
|
||||
.runInBackground(
|
||||
_keyFromPassphrase,
|
||||
_KeyFromPassphraseArgs(
|
||||
passphrase: passphrase,
|
||||
info: content.passphrase!,
|
||||
),
|
||||
)
|
||||
.timeout(Duration(seconds: 10));
|
||||
privateKey = await Future.value(
|
||||
client.nativeImplementations.keyFromPassphrase(
|
||||
KeyFromPassphraseArgs(
|
||||
passphrase: passphrase,
|
||||
info: content.passphrase!,
|
||||
),
|
||||
),
|
||||
).timeout(Duration(seconds: 10));
|
||||
} else {
|
||||
// we need to just generate a new key from scratch
|
||||
privateKey = Uint8List.fromList(uc.secureRandomBytes(ssssKeyLength));
|
||||
|
|
@ -657,15 +656,14 @@ class OpenSSSS {
|
|||
throw Exception(
|
||||
'Tried to unlock with passphrase while key does not have a passphrase');
|
||||
}
|
||||
privateKey = await ssss.client
|
||||
.runInBackground(
|
||||
_keyFromPassphrase,
|
||||
_KeyFromPassphraseArgs(
|
||||
passphrase: passphrase,
|
||||
info: keyData.passphrase!,
|
||||
),
|
||||
)
|
||||
.timeout(Duration(seconds: 10));
|
||||
privateKey = await Future.value(
|
||||
ssss.client.nativeImplementations.keyFromPassphrase(
|
||||
KeyFromPassphraseArgs(
|
||||
passphrase: passphrase,
|
||||
info: keyData.passphrase!,
|
||||
),
|
||||
),
|
||||
).timeout(Duration(seconds: 10));
|
||||
} else if (recoveryKey != null) {
|
||||
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
|
||||
} else {
|
||||
|
|
@ -743,13 +741,15 @@ class OpenSSSS {
|
|||
}
|
||||
}
|
||||
|
||||
class _KeyFromPassphraseArgs {
|
||||
class KeyFromPassphraseArgs {
|
||||
final String passphrase;
|
||||
final PassphraseInfo info;
|
||||
|
||||
_KeyFromPassphraseArgs({required this.passphrase, required this.info});
|
||||
KeyFromPassphraseArgs({required this.passphrase, required this.info});
|
||||
}
|
||||
|
||||
Future<Uint8List> _keyFromPassphrase(_KeyFromPassphraseArgs args) async {
|
||||
/// you would likely want to use [NativeImplementations] and
|
||||
/// [Client.nativeImplementations] instead
|
||||
Future<Uint8List> generateKeyFromPassphrase(KeyFromPassphraseArgs args) async {
|
||||
return await SSSS.keyFromPassphrase(args.passphrase, args.info);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export 'src/utils/matrix_file.dart';
|
|||
export 'src/utils/matrix_id_string_extension.dart';
|
||||
export 'src/utils/matrix_default_localizations.dart';
|
||||
export 'src/utils/matrix_localizations.dart';
|
||||
export 'src/utils/native_implementations.dart';
|
||||
export 'src/utils/push_notification.dart';
|
||||
export 'src/utils/receipt.dart';
|
||||
export 'src/utils/sync_update_extension.dart';
|
||||
|
|
@ -56,3 +57,9 @@ export 'src/utils/uri_extension.dart';
|
|||
|
||||
export 'msc_extensions/extension_recent_emoji/recent_emoji.dart';
|
||||
export 'msc_extensions/msc_1236_widgets/msc_1236_widgets.dart';
|
||||
|
||||
export 'src/utils/web_worker/web_worker_stub.dart'
|
||||
if (dart.library.html) 'src/utils/web_worker/web_worker.dart';
|
||||
|
||||
export 'src/utils/web_worker/native_implementations_web_worker_stub.dart'
|
||||
if (dart.library.html) 'src/utils/web_worker/native_implementations_web_worker.dart';
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import 'package:matrix/src/utils/sync_update_item_count.dart';
|
|||
import '../encryption.dart';
|
||||
import '../matrix.dart';
|
||||
import 'models/timeline_chunk.dart';
|
||||
import 'utils/compute_callback.dart';
|
||||
import 'utils/multilock.dart';
|
||||
import 'utils/run_benchmarked.dart';
|
||||
|
||||
|
|
@ -90,11 +91,13 @@ class Client extends MatrixApi {
|
|||
final Map<String, FutureOr<String?> Function(CommandArgs)> commands = {};
|
||||
final Filter syncFilter;
|
||||
|
||||
final NativeImplementations nativeImplementations;
|
||||
|
||||
String? syncFilterId;
|
||||
|
||||
final Future<R> Function<Q, R>(FutureOr<R> Function(Q), Q,
|
||||
{String debugLabel})? compute;
|
||||
final ComputeCallback? compute;
|
||||
|
||||
@Deprecated('Use [nativeImplementations] instead')
|
||||
Future<T> runInBackground<T, U>(
|
||||
FutureOr<T> Function(U arg) function, U arg) async {
|
||||
final compute = this.compute;
|
||||
|
|
@ -142,8 +145,8 @@ class Client extends MatrixApi {
|
|||
/// If your client supports more login types like login with token or SSO, then add this to
|
||||
/// [supportedLoginTypes]. Set a custom [syncFilter] if you like. By default the app
|
||||
/// will use lazy_load_members.
|
||||
/// Set [compute] to the Flutter compute method to enable the SDK to run some
|
||||
/// code in background.
|
||||
/// Set [nativeImplementations] to [NativeImplementationsIsolate] in order to
|
||||
/// enable the SDK to compute some code in background.
|
||||
/// Set [timelineEventTimeout] to the preferred time the Client should retry
|
||||
/// sending events on connection problems or to `Duration.zero` to disable it.
|
||||
/// Set [customImageResizer] to your own implementation for a more advanced
|
||||
|
|
@ -165,7 +168,8 @@ class Client extends MatrixApi {
|
|||
Set<String>? supportedLoginTypes,
|
||||
this.mxidLocalPartFallback = true,
|
||||
this.formatLocalpart = true,
|
||||
this.compute,
|
||||
@Deprecated('Use [nativeImplementations] instead') this.compute,
|
||||
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
||||
Level? logLevel,
|
||||
Filter? syncFilter,
|
||||
this.sendTimelineEventTimeout = const Duration(minutes: 1),
|
||||
|
|
@ -182,6 +186,9 @@ class Client extends MatrixApi {
|
|||
supportedLoginTypes =
|
||||
supportedLoginTypes ?? {AuthenticationTypes.password},
|
||||
verificationMethods = verificationMethods ?? <KeyVerificationMethod>{},
|
||||
nativeImplementations = compute != null
|
||||
? NativeImplementationsIsolate(compute)
|
||||
: nativeImplementations,
|
||||
super(
|
||||
httpClient:
|
||||
VariableTimeoutHttpClient(httpClient ?? http.Client())) {
|
||||
|
|
@ -227,7 +234,10 @@ class Client extends MatrixApi {
|
|||
String? _deviceName;
|
||||
|
||||
// for group calls
|
||||
// A unique identifier used for resolving duplicate group call sessions from a given device. When the session_id field changes from an incoming m.call.member event, any existing calls from this device in this call should be terminated. The id is generated once per client load.
|
||||
// A unique identifier used for resolving duplicate group call
|
||||
// sessions from a given device. When the session_id field changes from
|
||||
// an incoming m.call.member event, any existing calls from this device in
|
||||
// this call should be terminated. The id is generated once per client load.
|
||||
String? get groupCallSessionId => _groupCallSessionId;
|
||||
String? _groupCallSessionId;
|
||||
|
||||
|
|
@ -2906,5 +2916,6 @@ class HomeserverSummary {
|
|||
class ArchivedRoom {
|
||||
final Room room;
|
||||
final Timeline timeline;
|
||||
|
||||
ArchivedRoom({required this.room, required this.timeline});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -617,8 +617,8 @@ class Event extends MatrixEvent {
|
|||
k: fileMap['key']['k'],
|
||||
sha256: fileMap['hashes']['sha256'],
|
||||
);
|
||||
uint8list = await room.client.runInBackground<Uint8List?, EncryptedFile>(
|
||||
decryptFile, encryptedFile);
|
||||
uint8list =
|
||||
await room.client.nativeImplementations.decryptFile(encryptedFile);
|
||||
if (uint8list == null) {
|
||||
throw ('Unable to decrypt file');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -721,7 +721,7 @@ class Room {
|
|||
FileSendingStatus.generatingThumbnail.name;
|
||||
await _handleFakeSync(syncUpdate);
|
||||
thumbnail ??= await file.generateThumbnail(
|
||||
compute: client.runInBackground,
|
||||
nativeImplementations: client.nativeImplementations,
|
||||
customImageResizer: client.customImageResizer,
|
||||
);
|
||||
if (shrinkImageMaxDimension != null) {
|
||||
|
|
@ -730,7 +730,7 @@ class Room {
|
|||
name: file.name,
|
||||
maxDimension: shrinkImageMaxDimension,
|
||||
customImageResizer: client.customImageResizer,
|
||||
compute: client.runInBackground,
|
||||
nativeImplementations: client.nativeImplementations,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -942,7 +942,7 @@ class Room {
|
|||
? inReplyTo.formattedText
|
||||
: htmlEscape.convert(inReplyTo.body).replaceAll('\n', '<br>'))
|
||||
.replaceAll(
|
||||
RegExp(r'<mx-reply>.*<\/mx-reply>',
|
||||
RegExp(r'<mx-reply>.*</mx-reply>',
|
||||
caseSensitive: false, multiLine: false, dotAll: true),
|
||||
'');
|
||||
final repliedHtml = content.tryGet<String>('formatted_body') ??
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
typedef ComputeCallback = Future<R> Function<Q, R>(
|
||||
FutureOr<R> Function(Q message) callback, Q message,
|
||||
{String? debugLabel});
|
||||
|
||||
// keep types in sync with [computeCallbackFromRunInBackground]
|
||||
typedef ComputeRunner = Future<T> Function<T, U>(
|
||||
FutureOr<T> Function(U arg) function, U arg);
|
||||
|
||||
ComputeCallback computeCallbackFromRunInBackground(ComputeRunner runner) {
|
||||
return <U, T>(FutureOr<T> Function(U arg) callback, U arg,
|
||||
{String? debugLabel}) =>
|
||||
runner.call(callback, arg);
|
||||
}
|
||||
|
|
@ -48,7 +48,9 @@ Future<EncryptedFile> encryptFile(Uint8List input) async {
|
|||
);
|
||||
}
|
||||
|
||||
Future<Uint8List?> decryptFile(EncryptedFile input) async {
|
||||
/// you would likely want to use [NativeImplementations] and
|
||||
/// [Client.nativeImplementations] instead
|
||||
Future<Uint8List?> decryptFileImplementation(EncryptedFile input) async {
|
||||
if (base64.encode(await sha256(input.data)) !=
|
||||
base64.normalize(input.sha256)) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import 'package:image/image.dart';
|
|||
import 'package:mime/mime.dart';
|
||||
|
||||
import '../../matrix.dart';
|
||||
import 'compute_callback.dart';
|
||||
|
||||
class MatrixFile {
|
||||
final Uint8List bytes;
|
||||
|
|
@ -78,15 +79,18 @@ class MatrixImageFile extends MatrixFile {
|
|||
super(bytes: bytes, name: name, mimeType: mimeType);
|
||||
|
||||
/// Creates a new image file and calculates the width, height and blurhash.
|
||||
static Future<MatrixImageFile> create(
|
||||
{required Uint8List bytes,
|
||||
required String name,
|
||||
String? mimeType,
|
||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
||||
compute}) async {
|
||||
final metaData = compute != null
|
||||
? await compute(_calcMetadata, bytes)
|
||||
: _calcMetadata(bytes);
|
||||
static Future<MatrixImageFile> create({
|
||||
required Uint8List bytes,
|
||||
required String name,
|
||||
String? mimeType,
|
||||
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
|
||||
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
||||
}) async {
|
||||
if (compute != null) {
|
||||
nativeImplementations =
|
||||
NativeImplementationsIsolate.fromRunInBackground(compute);
|
||||
}
|
||||
final metaData = await nativeImplementations.calcImageMetadata(bytes);
|
||||
|
||||
return MatrixImageFile(
|
||||
bytes: metaData?.bytes ?? bytes,
|
||||
|
|
@ -101,22 +105,27 @@ class MatrixImageFile extends MatrixFile {
|
|||
/// Builds a [MatrixImageFile] and shrinks it in order to reduce traffic.
|
||||
/// If shrinking does not work (e.g. for unsupported MIME types), the
|
||||
/// initial image is preserved without shrinking it.
|
||||
static Future<MatrixImageFile> shrink(
|
||||
{required Uint8List bytes,
|
||||
required String name,
|
||||
int maxDimension = 1600,
|
||||
String? mimeType,
|
||||
Future<MatrixImageFileResizedResponse?> Function(
|
||||
MatrixImageFileResizeArguments)?
|
||||
customImageResizer,
|
||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
||||
compute}) async {
|
||||
static Future<MatrixImageFile> shrink({
|
||||
required Uint8List bytes,
|
||||
required String name,
|
||||
int maxDimension = 1600,
|
||||
String? mimeType,
|
||||
Future<MatrixImageFileResizedResponse?> Function(
|
||||
MatrixImageFileResizeArguments)?
|
||||
customImageResizer,
|
||||
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
|
||||
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
||||
}) async {
|
||||
if (compute != null) {
|
||||
nativeImplementations =
|
||||
NativeImplementationsIsolate.fromRunInBackground(compute);
|
||||
}
|
||||
final image = MatrixImageFile(name: name, mimeType: mimeType, bytes: bytes);
|
||||
|
||||
return await image.generateThumbnail(
|
||||
dimension: maxDimension,
|
||||
customImageResizer: customImageResizer,
|
||||
compute: compute) ??
|
||||
nativeImplementations: nativeImplementations) ??
|
||||
image;
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +150,7 @@ class MatrixImageFile extends MatrixFile {
|
|||
|
||||
@override
|
||||
String get msgType => 'm.image';
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get info => ({
|
||||
...super.info,
|
||||
|
|
@ -151,13 +161,18 @@ class MatrixImageFile extends MatrixFile {
|
|||
|
||||
/// Computes a thumbnail for the image.
|
||||
/// Also sets height and width on the original image if they were unset.
|
||||
Future<MatrixImageFile?> generateThumbnail(
|
||||
{int dimension = Client.defaultThumbnailSize,
|
||||
Future<MatrixImageFileResizedResponse?> Function(
|
||||
MatrixImageFileResizeArguments)?
|
||||
customImageResizer,
|
||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
||||
compute}) async {
|
||||
Future<MatrixImageFile?> generateThumbnail({
|
||||
int dimension = Client.defaultThumbnailSize,
|
||||
Future<MatrixImageFileResizedResponse?> Function(
|
||||
MatrixImageFileResizeArguments)?
|
||||
customImageResizer,
|
||||
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
|
||||
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
||||
}) async {
|
||||
if (compute != null) {
|
||||
nativeImplementations =
|
||||
NativeImplementationsIsolate.fromRunInBackground(compute);
|
||||
}
|
||||
final arguments = MatrixImageFileResizeArguments(
|
||||
bytes: bytes,
|
||||
maxDimension: dimension,
|
||||
|
|
@ -166,19 +181,17 @@ class MatrixImageFile extends MatrixFile {
|
|||
);
|
||||
final resizedData = customImageResizer != null
|
||||
? await customImageResizer(arguments)
|
||||
: compute != null
|
||||
? await compute(_resize, arguments)
|
||||
: _resize(arguments);
|
||||
: await nativeImplementations.shrinkImage(arguments);
|
||||
|
||||
if (resizedData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// we should take the opportinity to update the image dimmension
|
||||
// we should take the opportunity to update the image dimension
|
||||
setImageSizeIfNull(
|
||||
width: resizedData.originalWidth, height: resizedData.originalHeight);
|
||||
|
||||
// the thumbnail should rather return null than the unshrinked image
|
||||
// the thumbnail should rather return null than the enshrined image
|
||||
if (resizedData.width > dimension || resizedData.height > dimension) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -194,7 +207,10 @@ class MatrixImageFile extends MatrixFile {
|
|||
return thumbnailFile;
|
||||
}
|
||||
|
||||
static MatrixImageFileResizedResponse? _calcMetadata(Uint8List bytes) {
|
||||
/// you would likely want to use [NativeImplementations] and
|
||||
/// [Client.nativeImplementations] instead
|
||||
static MatrixImageFileResizedResponse? calcMetadataImplementation(
|
||||
Uint8List bytes) {
|
||||
final image = decodeImage(bytes);
|
||||
if (image == null) return null;
|
||||
|
||||
|
|
@ -210,7 +226,9 @@ class MatrixImageFile extends MatrixFile {
|
|||
);
|
||||
}
|
||||
|
||||
static MatrixImageFileResizedResponse? _resize(
|
||||
/// you would likely want to use [NativeImplementations] and
|
||||
/// [Client.nativeImplementations] instead
|
||||
static MatrixImageFileResizedResponse? resizeImplementation(
|
||||
MatrixImageFileResizeArguments arguments) {
|
||||
final image = decodeImage(arguments.bytes);
|
||||
|
||||
|
|
@ -255,6 +273,28 @@ class MatrixImageFileResizedResponse {
|
|||
this.originalWidth,
|
||||
this.blurhash,
|
||||
});
|
||||
|
||||
factory MatrixImageFileResizedResponse.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) =>
|
||||
MatrixImageFileResizedResponse(
|
||||
bytes: Uint8List.fromList(
|
||||
(json['bytes'] as Iterable<dynamic>).whereType<int>().toList()),
|
||||
width: json['width'],
|
||||
height: json['height'],
|
||||
originalHeight: json['originalHeight'],
|
||||
originalWidth: json['originalWidth'],
|
||||
blurhash: json['blurhash'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'bytes': bytes,
|
||||
'width': width,
|
||||
'height': height,
|
||||
if (blurhash != null) 'blurhash': blurhash,
|
||||
if (originalHeight != null) 'originalHeight': originalHeight,
|
||||
if (originalWidth != null) 'originalWidth': originalWidth,
|
||||
};
|
||||
}
|
||||
|
||||
class MatrixImageFileResizeArguments {
|
||||
|
|
@ -269,6 +309,21 @@ class MatrixImageFileResizeArguments {
|
|||
required this.fileName,
|
||||
required this.calcBlurhash,
|
||||
});
|
||||
|
||||
factory MatrixImageFileResizeArguments.fromJson(Map<String, dynamic> json) =>
|
||||
MatrixImageFileResizeArguments(
|
||||
bytes: json['bytes'],
|
||||
maxDimension: json['maxDimension'],
|
||||
fileName: json['fileName'],
|
||||
calcBlurhash: json['calcBlurhash'],
|
||||
);
|
||||
|
||||
Map<String, Object> toJson() => {
|
||||
'bytes': bytes,
|
||||
'maxDimension': maxDimension,
|
||||
'fileName': fileName,
|
||||
'calcBlurhash': calcBlurhash,
|
||||
};
|
||||
}
|
||||
|
||||
class MatrixVideoFile extends MatrixFile {
|
||||
|
|
@ -284,8 +339,10 @@ class MatrixVideoFile extends MatrixFile {
|
|||
this.height,
|
||||
this.duration})
|
||||
: super(bytes: bytes, name: name, mimeType: mimeType);
|
||||
|
||||
@override
|
||||
String get msgType => 'm.video';
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get info => ({
|
||||
...super.info,
|
||||
|
|
@ -304,8 +361,10 @@ class MatrixAudioFile extends MatrixFile {
|
|||
String? mimeType,
|
||||
this.duration})
|
||||
: super(bytes: bytes, name: name, mimeType: mimeType);
|
||||
|
||||
@override
|
||||
String get msgType => 'm.audio';
|
||||
|
||||
@override
|
||||
Map<String, dynamic> get info => ({
|
||||
...super.info,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:matrix/encryption.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'compute_callback.dart';
|
||||
|
||||
/// provides native implementations for demanding arithmetic operations
|
||||
/// in order to prevent the UI from blocking
|
||||
///
|
||||
/// possible implementations might be:
|
||||
/// - native code
|
||||
/// - another Dart isolate
|
||||
/// - a web worker
|
||||
/// - a dummy implementations
|
||||
///
|
||||
/// Rules for extension (important for [noSuchMethod] implementations)
|
||||
/// - always only accept exactly *one* positioned argument
|
||||
/// - catch the corresponding case in [NativeImplementations.noSuchMethod]
|
||||
/// - always write a dummy implementations
|
||||
abstract class NativeImplementations {
|
||||
const NativeImplementations();
|
||||
|
||||
/// a dummy implementation executing all calls in the same thread causing
|
||||
/// the UI to likely freeze
|
||||
static const dummy = NativeImplementationsDummy();
|
||||
|
||||
FutureOr<RoomKeys> generateUploadKeys(GenerateUploadKeysArgs args);
|
||||
|
||||
FutureOr<Uint8List> keyFromPassphrase(KeyFromPassphraseArgs args);
|
||||
|
||||
FutureOr<Uint8List?> decryptFile(EncryptedFile file);
|
||||
|
||||
FutureOr<MatrixImageFileResizedResponse?> shrinkImage(
|
||||
MatrixImageFileResizeArguments args);
|
||||
|
||||
FutureOr<MatrixImageFileResizedResponse?> calcImageMetadata(Uint8List bytes);
|
||||
|
||||
@override
|
||||
|
||||
/// this implementation will catch any non-implemented method
|
||||
dynamic noSuchMethod(Invocation invocation) {
|
||||
final dynamic argument = invocation.positionalArguments.single;
|
||||
final memberName = invocation.memberName.toString().split('"')[1];
|
||||
|
||||
Logs().w(
|
||||
'Missing implementations of Client.nativeImplementations.$memberName. '
|
||||
'You should consider implementing it. '
|
||||
'Fallback from NativeImplementations.dummy used.',
|
||||
);
|
||||
switch (memberName) {
|
||||
case 'generateUploadKeys':
|
||||
return dummy.generateUploadKeys(argument);
|
||||
case 'keyFromPassphrase':
|
||||
return dummy.keyFromPassphrase(argument);
|
||||
case 'decryptFile':
|
||||
return dummy.decryptFile(argument);
|
||||
case 'shrinkImage':
|
||||
return dummy.shrinkImage(argument);
|
||||
case 'calcImageMetadata':
|
||||
return dummy.calcImageMetadata(argument);
|
||||
default:
|
||||
return super.noSuchMethod(invocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NativeImplementationsDummy extends NativeImplementations {
|
||||
const NativeImplementationsDummy();
|
||||
|
||||
@override
|
||||
Future<Uint8List?> decryptFile(EncryptedFile file) {
|
||||
return decryptFileImplementation(file);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RoomKeys> generateUploadKeys(GenerateUploadKeysArgs args) async {
|
||||
return generateUploadKeysImplementation(args);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> keyFromPassphrase(KeyFromPassphraseArgs args) {
|
||||
return generateKeyFromPassphrase(args);
|
||||
}
|
||||
|
||||
@override
|
||||
MatrixImageFileResizedResponse? shrinkImage(
|
||||
MatrixImageFileResizeArguments args) {
|
||||
return MatrixImageFile.resizeImplementation(args);
|
||||
}
|
||||
|
||||
@override
|
||||
MatrixImageFileResizedResponse? calcImageMetadata(Uint8List bytes) {
|
||||
return MatrixImageFile.calcMetadataImplementation(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// a [NativeImplementations] based on Flutter's `compute` function
|
||||
///
|
||||
/// this implementations simply wraps the given [compute] function around
|
||||
/// the implementation of [NativeImplementations.dummy]
|
||||
class NativeImplementationsIsolate extends NativeImplementations {
|
||||
/// pass by Flutter's compute function here
|
||||
final ComputeCallback compute;
|
||||
|
||||
NativeImplementationsIsolate(this.compute);
|
||||
|
||||
/// creates a [NativeImplementationsIsolate] based on a [ComputeRunner] as
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
/// known from [Client.runInBackground]
|
||||
factory NativeImplementationsIsolate.fromRunInBackground(
|
||||
ComputeRunner runInBackground) {
|
||||
return NativeImplementationsIsolate(
|
||||
computeCallbackFromRunInBackground(runInBackground),
|
||||
);
|
||||
}
|
||||
|
||||
Future<T> runInBackground<T, U>(
|
||||
FutureOr<T> Function(U arg) function, U arg) async {
|
||||
final compute = this.compute;
|
||||
return await compute(function, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List?> decryptFile(EncryptedFile file) {
|
||||
return runInBackground<Uint8List?, EncryptedFile>(
|
||||
NativeImplementations.dummy.decryptFile,
|
||||
file,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RoomKeys> generateUploadKeys(GenerateUploadKeysArgs args) async {
|
||||
return runInBackground<RoomKeys, GenerateUploadKeysArgs>(
|
||||
NativeImplementations.dummy.generateUploadKeys,
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> keyFromPassphrase(KeyFromPassphraseArgs args) {
|
||||
return runInBackground<Uint8List, KeyFromPassphraseArgs>(
|
||||
NativeImplementations.dummy.keyFromPassphrase,
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MatrixImageFileResizedResponse?> shrinkImage(
|
||||
MatrixImageFileResizeArguments args) {
|
||||
return runInBackground<MatrixImageFileResizedResponse?,
|
||||
MatrixImageFileResizeArguments>(
|
||||
NativeImplementations.dummy.shrinkImage,
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<MatrixImageFileResizedResponse?> calcImageMetadata(Uint8List bytes) {
|
||||
return runInBackground<MatrixImageFileResizedResponse?, Uint8List>(
|
||||
NativeImplementations.dummy.calcImageMetadata,
|
||||
bytes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:html';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:matrix/encryption.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class NativeImplementationsWebWorker extends NativeImplementations {
|
||||
final Worker worker;
|
||||
final Duration timeout;
|
||||
final WebWorkerStackTraceCallback onStackTrace;
|
||||
|
||||
final Map<double, Completer<dynamic>> _completers = {};
|
||||
final _random = Random();
|
||||
|
||||
/// the default handler for stackTraces in web workers
|
||||
static StackTrace defaultStackTraceHandler(String obfuscatedStackTrace) {
|
||||
return StackTrace.fromString(obfuscatedStackTrace);
|
||||
}
|
||||
|
||||
NativeImplementationsWebWorker(
|
||||
Uri href, {
|
||||
this.timeout = const Duration(seconds: 30),
|
||||
this.onStackTrace = defaultStackTraceHandler,
|
||||
}) : worker = Worker(href.toString()) {
|
||||
worker.onMessage.listen(_handleIncomingMessage);
|
||||
}
|
||||
|
||||
Future<T> operation<T, U>(WebWorkerOperations name, U argument) async {
|
||||
final label = _random.nextDouble();
|
||||
final completer = Completer<T>();
|
||||
_completers[label] = completer;
|
||||
final message = WebWorkerData(label, name, argument);
|
||||
worker.postMessage(message.toJson());
|
||||
|
||||
return completer.future.timeout(timeout);
|
||||
}
|
||||
|
||||
void _handleIncomingMessage(MessageEvent event) {
|
||||
final data = event.data;
|
||||
// don't forget handling errors of our second thread...
|
||||
if (data['label'] == 'stacktrace') {
|
||||
final origin = event.data['origin'];
|
||||
final completer = _completers[origin];
|
||||
|
||||
final error = event.data['error']!;
|
||||
|
||||
Future.value(
|
||||
onStackTrace.call(event.data['stacktrace'] as String),
|
||||
).then(
|
||||
(stackTrace) => completer?.completeError(
|
||||
WebWorkerError(error: error, stackTrace: stackTrace),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final response = WebWorkerData.fromJson(event.data);
|
||||
_completers[response.label]!.complete(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MatrixImageFileResizedResponse?> calcImageMetadata(
|
||||
Uint8List bytes) async {
|
||||
try {
|
||||
final result = await operation<Map<dynamic, dynamic>, Uint8List>(
|
||||
WebWorkerOperations.calcImageMetadata,
|
||||
bytes,
|
||||
);
|
||||
return MatrixImageFileResizedResponse.fromJson(Map.from(result));
|
||||
} catch (e, s) {
|
||||
Logs().e('Web worker computation error. Fallback to main thread', e, s);
|
||||
return NativeImplementations.dummy.calcImageMetadata(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MatrixImageFileResizedResponse?> shrinkImage(
|
||||
MatrixImageFileResizeArguments args) async {
|
||||
try {
|
||||
final result =
|
||||
await operation<Map<dynamic, dynamic>, Map<String, dynamic>>(
|
||||
WebWorkerOperations.shrinkImage,
|
||||
args.toJson(),
|
||||
);
|
||||
return MatrixImageFileResizedResponse.fromJson(Map.from(result));
|
||||
} catch (e, s) {
|
||||
Logs().e('Web worker computation error. Fallback to main thread', e, s);
|
||||
return NativeImplementations.dummy.shrinkImage(args);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RoomKeys> generateUploadKeys(GenerateUploadKeysArgs args) async {
|
||||
try {
|
||||
final result =
|
||||
await operation<Map<dynamic, dynamic>, Map<String, dynamic>>(
|
||||
WebWorkerOperations.generateUploadKeys,
|
||||
args.toJson(),
|
||||
);
|
||||
return RoomKeys.fromJson(Map.from(result));
|
||||
} catch (e, s) {
|
||||
Logs().e('Web worker computation error. Fallback to main thread', e, s);
|
||||
return NativeImplementations.dummy.generateUploadKeys(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WebWorkerData {
|
||||
final Object? label;
|
||||
final WebWorkerOperations? name;
|
||||
final Object? data;
|
||||
|
||||
const WebWorkerData(this.label, this.name, this.data);
|
||||
|
||||
factory WebWorkerData.fromJson(LinkedHashMap<dynamic, dynamic> data) =>
|
||||
WebWorkerData(
|
||||
data['label'],
|
||||
data.containsKey('name')
|
||||
? WebWorkerOperations.values[data['name']]
|
||||
: null,
|
||||
data['data'],
|
||||
);
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'label': label,
|
||||
if (name != null) 'name': name!.index,
|
||||
'data': data,
|
||||
};
|
||||
}
|
||||
|
||||
enum WebWorkerOperations {
|
||||
shrinkImage,
|
||||
calcImageMetadata,
|
||||
generateUploadKeys,
|
||||
}
|
||||
|
||||
class WebWorkerError extends Error {
|
||||
/// the error thrown in the web worker. Usually a [String]
|
||||
final Object? error;
|
||||
|
||||
/// de-serialized [StackTrace]
|
||||
@override
|
||||
final StackTrace stackTrace;
|
||||
|
||||
WebWorkerError({required this.error, required this.stackTrace});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$error, $stackTrace';
|
||||
}
|
||||
}
|
||||
|
||||
/// converts a stringifyed, obfuscated [StackTrace] into a [StackTrace]
|
||||
typedef WebWorkerStackTraceCallback = FutureOr<StackTrace> Function(
|
||||
String obfuscatedStackTrace);
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class NativeImplementationsWebWorker extends NativeImplementations {
|
||||
/// the default handler for stackTraces in web workers
|
||||
static StackTrace defaultStackTraceHandler(String obfuscatedStackTrace) {
|
||||
return StackTrace.fromString(obfuscatedStackTrace);
|
||||
}
|
||||
|
||||
NativeImplementationsWebWorker(
|
||||
Uri href, {
|
||||
Duration timeout = const Duration(seconds: 30),
|
||||
WebWorkerStackTraceCallback onStackTrace = defaultStackTraceHandler,
|
||||
});
|
||||
}
|
||||
|
||||
class WebWorkerError extends Error {
|
||||
/// the error thrown in the web worker. Usually a [String]
|
||||
final Object? error;
|
||||
|
||||
/// de-serialized [StackTrace]
|
||||
@override
|
||||
final StackTrace stackTrace;
|
||||
|
||||
WebWorkerError({required this.error, required this.stackTrace});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$error, $stackTrace';
|
||||
}
|
||||
}
|
||||
|
||||
/// converts a stringifyed, obfuscated [StackTrace] into a [StackTrace]
|
||||
typedef WebWorkerStackTraceCallback = FutureOr<StackTrace> Function(
|
||||
String obfuscatedStackTrace);
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
// ignore_for_file: avoid_print
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
import 'dart:indexed_db';
|
||||
import 'dart:js';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:js/js.dart';
|
||||
import 'package:js/js_util.dart';
|
||||
|
||||
import 'package:matrix/encryption.dart';
|
||||
import 'package:matrix/matrix.dart' hide Event;
|
||||
import 'native_implementations_web_worker.dart';
|
||||
|
||||
///
|
||||
///
|
||||
/// CAUTION: THIS FILE NEEDS TO BE MANUALLY COMPILED
|
||||
///
|
||||
/// 1. in your project, create a file `web/web_worker.dart`
|
||||
/// 2. add the following contents:
|
||||
/// ```dart
|
||||
/// import 'package:hive/hive.dart';
|
||||
///
|
||||
/// Future<void> main() => startWebWorker();
|
||||
/// ```
|
||||
/// 3. compile the file using:
|
||||
/// ```shell
|
||||
/// dart compile js -o web/web_worker.dart.js -m web/web_worker.dart
|
||||
/// ```
|
||||
///
|
||||
/// You should not check in that file into your VCS. Instead, you should compile
|
||||
/// the web worker in your CI pipeline.
|
||||
///
|
||||
|
||||
@pragma('dart2js:tryInline')
|
||||
Future<void> startWebWorker() async {
|
||||
print('[native implementations worker]: Starting...');
|
||||
setProperty(
|
||||
context['self'] as Object,
|
||||
'onmessage',
|
||||
allowInterop(
|
||||
(MessageEvent event) async {
|
||||
final data = event.data;
|
||||
try {
|
||||
final operation = WebWorkerData.fromJson(data);
|
||||
switch (operation.name) {
|
||||
case WebWorkerOperations.shrinkImage:
|
||||
final result = MatrixImageFile.resizeImplementation(
|
||||
MatrixImageFileResizeArguments.fromJson(
|
||||
Map.from(operation.data as Map)));
|
||||
sendResponse(operation.label as double, result?.toJson());
|
||||
break;
|
||||
case WebWorkerOperations.calcImageMetadata:
|
||||
final result = MatrixImageFile.calcMetadataImplementation(
|
||||
Uint8List.fromList(
|
||||
(operation.data as JsArray).whereType<int>().toList()));
|
||||
sendResponse(operation.label as double, result?.toJson());
|
||||
break;
|
||||
case WebWorkerOperations.generateUploadKeys:
|
||||
final result = generateUploadKeysImplementation(
|
||||
GenerateUploadKeysArgs.fromJson(
|
||||
Map.from(operation.data as Map),
|
||||
),
|
||||
);
|
||||
sendResponse(operation.label as double, result.toJson());
|
||||
break;
|
||||
default:
|
||||
throw NullThrownError();
|
||||
}
|
||||
} on Event catch (e, s) {
|
||||
allowInterop(_replyError)
|
||||
.call((e.target as Request).error, s, data['label'] as double);
|
||||
} catch (e, s) {
|
||||
allowInterop(_replyError).call(e, s, data['label'] as double);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void sendResponse(double label, dynamic response) {
|
||||
try {
|
||||
self.postMessage({
|
||||
'label': label,
|
||||
'data': response,
|
||||
});
|
||||
} catch (e, s) {
|
||||
print('[native implementations worker] Error responding: $e, $s');
|
||||
}
|
||||
}
|
||||
|
||||
void _replyError(Object? error, StackTrace stackTrace, double origin) {
|
||||
if (error != null) {
|
||||
try {
|
||||
final jsError = jsify(error);
|
||||
if (jsError != null) {
|
||||
error = jsError;
|
||||
}
|
||||
} catch (e) {
|
||||
error = error.toString();
|
||||
}
|
||||
}
|
||||
try {
|
||||
self.postMessage({
|
||||
'label': 'stacktrace',
|
||||
'origin': origin,
|
||||
'error': error,
|
||||
'stacktrace': stackTrace.toString(),
|
||||
});
|
||||
} catch (e, s) {
|
||||
print('[native implementations worker] Error responding: $e, $s');
|
||||
}
|
||||
}
|
||||
|
||||
/// represents the [WorkerGlobalScope] the worker currently runs in.
|
||||
@JS('self')
|
||||
external WorkerGlobalScope get self;
|
||||
|
||||
/// adding all missing WebWorker-only properties to the [WorkerGlobalScope]
|
||||
extension on WorkerGlobalScope {
|
||||
void postMessage(Object data) {
|
||||
callMethod(self, 'postMessage', [jsify(data)]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import 'dart:async';
|
||||
|
||||
///
|
||||
///
|
||||
/// CAUTION: THIS FILE NEEDS TO BE MANUALLY COMPILED
|
||||
///
|
||||
/// 1. in your project, create a file `web/web_worker.dart`
|
||||
/// 2. add the following contents:
|
||||
/// ```dart
|
||||
/// import 'package:hive/hive.dart';
|
||||
///
|
||||
/// Future<void> main() => startWebWorker();
|
||||
/// ```
|
||||
/// 3. compile the file using:
|
||||
/// ```shell
|
||||
/// dart compile js -o web/web_worker.dart.js -m web/web_worker.dart
|
||||
/// ```
|
||||
///
|
||||
/// You should not check in that file into your VCS. Instead, you should compile
|
||||
/// the web worker in your CI pipeline.
|
||||
///
|
||||
|
||||
Future<void> startWebWorker() => Future.value();
|
||||
Loading…
Reference in New Issue