Add Cross-Signing

This commit is contained in:
Sorunome 2020-06-25 14:29:06 +00:00 committed by Christian Pauly
parent 44bbb25f6e
commit 4550686829
13 changed files with 1149 additions and 74 deletions

View file

@ -4,9 +4,13 @@
- Chat app bar transparent - Chat app bar transparent
- Implement web file picker - Implement web file picker
- Minor design and UX improvements - Minor design and UX improvements
- Implement Cross Signing
- Restore keys from online key backup
### Changes: ### Changes:
- Show presences of users sharing a direct chat - Show presences of users sharing a direct chat
- Big refactoring - Big refactoring
### Fixes:
- Various fixes, including e2ee fixes and olm session recovery
# Version 0.14.0 - 2020-05-20 # Version 0.14.0 - 2020-05-20
### Features: ### Features:

View file

@ -1,6 +1,5 @@
import 'package:bubble/bubble.dart'; import 'package:bubble/bubble.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; import 'package:fluffychat/components/dialogs/simple_dialogs.dart';
import 'package:fluffychat/components/message_content.dart'; import 'package:fluffychat/components/message_content.dart';
import 'package:fluffychat/components/reply_content.dart'; import 'package:fluffychat/components/reply_content.dart';
@ -122,7 +121,7 @@ class Message extends StatelessWidget {
), ),
if (event.type == EventTypes.Encrypted && if (event.type == EventTypes.Encrypted &&
event.messageType == MessageTypes.BadEncrypted && event.messageType == MessageTypes.BadEncrypted &&
event.content['body'] == DecryptError.UNKNOWN_SESSION) event.content['can_request_session'] == true)
RaisedButton( RaisedButton(
color: color.withAlpha(100), color: color.withAlpha(100),
child: Text( child: Text(

View file

@ -14,6 +14,8 @@ import '../l10n/l10n.dart';
import '../utils/beautify_string_extension.dart'; import '../utils/beautify_string_extension.dart';
import '../utils/famedlysdk_store.dart'; import '../utils/famedlysdk_store.dart';
import 'avatar.dart'; import 'avatar.dart';
import '../views/key_verification.dart';
import '../utils/app_route.dart';
class Matrix extends StatefulWidget { class Matrix extends StatefulWidget {
static const String callNamespace = 'chat.fluffy.jitsi_call'; static const String callNamespace = 'chat.fluffy.jitsi_call';
@ -97,6 +99,7 @@ class MatrixState extends State<Matrix> {
}; };
StreamSubscription onRoomKeyRequestSub; StreamSubscription onRoomKeyRequestSub;
StreamSubscription onKeyVerificationRequestSub;
StreamSubscription onJitsiCallSub; StreamSubscription onJitsiCallSub;
void onJitsiCall(EventUpdate eventUpdate) { void onJitsiCall(EventUpdate eventUpdate) {
@ -159,7 +162,17 @@ class MatrixState extends State<Matrix> {
store = widget.store ?? Store(); store = widget.store ?? Store();
if (widget.client == null) { if (widget.client == null) {
debugPrint('[Matrix] Init matrix client'); debugPrint('[Matrix] Init matrix client');
client = Client(widget.clientName, debug: false); final Set verificationMethods = <KeyVerificationMethod>{
KeyVerificationMethod.numbers
};
if (!kIsWeb) {
// emojis don't show in web somehow
verificationMethods.add(KeyVerificationMethod.emoji);
}
client = Client(widget.clientName,
debug: false,
enableE2eeRecovery: true,
verificationMethods: verificationMethods);
onJitsiCallSub ??= client.onEvent.stream onJitsiCallSub ??= client.onEvent.stream
.where((e) => .where((e) =>
e.type == 'timeline' && e.type == 'timeline' &&
@ -184,6 +197,23 @@ class MatrixState extends State<Matrix> {
await request.forwardKey(); await request.forwardKey();
} }
}); });
onKeyVerificationRequestSub ??= client.onKeyVerificationRequest.stream
.listen((KeyVerification request) async {
if (await SimpleDialogs(context).askConfirmation(
titleText: L10n.of(context).newVerificationRequest,
contentText: L10n.of(context).askVerificationRequest(request.userId),
)) {
await request.acceptVerification();
await Navigator.of(context).push(
AppRoute.defaultRoute(
context,
KeyVerificationView(request: request),
),
);
} else {
await request.rejectVerification();
}
});
_initWithStore(); _initWithStore();
} else { } else {
client = widget.client; client = widget.client;
@ -210,6 +240,7 @@ class MatrixState extends State<Matrix> {
@override @override
void dispose() { void dispose() {
onRoomKeyRequestSub?.cancel(); onRoomKeyRequestSub?.cancel();
onKeyVerificationRequestSub?.cancel();
onJitsiCallSub?.cancel(); onJitsiCallSub?.cancel();
super.dispose(); super.dispose();
} }

View file

@ -1,10 +1,15 @@
{ {
"@@last_modified": "2020-05-15T15:34:50.065646", "@@last_modified": "2020-06-25T16:02:16.297192",
"About": "About", "About": "About",
"@About": { "@About": {
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"Accept": "Accept",
"@Accept": {
"type": "text",
"placeholders": {}
},
"acceptedTheInvitation": "{username} accepted the invitation", "acceptedTheInvitation": "{username} accepted the invitation",
"@acceptedTheInvitation": { "@acceptedTheInvitation": {
"type": "text", "type": "text",
@ -74,6 +79,28 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"askSSSSCache": "Please enter your secure store passphrase or recovery key to cache the keys.",
"@askSSSSCache": {
"type": "text",
"placeholders": {}
},
"askSSSSSign": "To be able to sign the other person, please enter your secure store passphrase or recovery key.",
"@askSSSSSign": {
"type": "text",
"placeholders": {}
},
"askSSSSVerify": "Please enter your secure store passphrase or recovery key to verify your session.",
"@askSSSSVerify": {
"type": "text",
"placeholders": {}
},
"askVerificationRequest": "Accept this verification request from {username}?",
"@askVerificationRequest": {
"type": "text",
"placeholders": {
"username": {}
}
},
"Authentication": "Authentication", "Authentication": "Authentication",
"@Authentication": { "@Authentication": {
"type": "text", "type": "text",
@ -102,6 +129,11 @@
"targetName": {} "targetName": {}
} }
}, },
"Block Device": "Block Device",
"@Block Device": {
"type": "text",
"placeholders": {}
},
"byDefaultYouWillBeConnectedTo": "By default you will be connected to {homeserver}", "byDefaultYouWillBeConnectedTo": "By default you will be connected to {homeserver}",
"@byDefaultYouWillBeConnectedTo": { "@byDefaultYouWillBeConnectedTo": {
"type": "text", "type": "text",
@ -109,6 +141,11 @@
"homeserver": {} "homeserver": {}
} }
}, },
"cachedKeys": "Successfully cached keys!",
"@cachedKeys": {
"type": "text",
"placeholders": {}
},
"Cancel": "Cancel", "Cancel": "Cancel",
"@Cancel": { "@Cancel": {
"type": "text", "type": "text",
@ -273,6 +310,16 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"compareEmojiMatch": "Compare and make sure the following emoji match those of the other device:",
"@compareEmojiMatch": {
"type": "text",
"placeholders": {}
},
"compareNumbersMatch": "Compare and make sure the following numbers match those of the other device:",
"@compareNumbersMatch": {
"type": "text",
"placeholders": {}
},
"Confirm": "Confirm", "Confirm": "Confirm",
"@Confirm": { "@Confirm": {
"type": "text", "type": "text",
@ -354,6 +401,16 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"crossSigningDisabled": "Cross-Signing is disabled",
"@crossSigningDisabled": {
"type": "text",
"placeholders": {}
},
"crossSigningEnabled": "Cross-Signing is enabled",
"@crossSigningEnabled": {
"type": "text",
"placeholders": {}
},
"Currently active": "Currently active", "Currently active": "Currently active",
"@Currently active": { "@Currently active": {
"type": "text", "type": "text",
@ -464,6 +521,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"Encryption": "Encryption",
"@Encryption": {
"type": "text",
"placeholders": {}
},
"Encryption algorithm": "Encryption algorithm", "Encryption algorithm": "Encryption algorithm",
"@Encryption algorithm": { "@Encryption algorithm": {
"type": "text", "type": "text",
@ -594,6 +656,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"incorrectPassphraseOrKey": "Incorrect passphrase or recovery key",
"@incorrectPassphraseOrKey": {
"type": "text",
"placeholders": {}
},
"Invite contact": "Invite contact", "Invite contact": "Invite contact",
"@Invite contact": { "@Invite contact": {
"type": "text", "type": "text",
@ -632,6 +699,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"isDeviceKeyCorrect": "Is the following device key correct?",
"@isDeviceKeyCorrect": {
"type": "text",
"placeholders": {}
},
"is typing...": "is typing...", "is typing...": "is typing...",
"@is typing...": { "@is typing...": {
"type": "text", "type": "text",
@ -649,6 +721,16 @@
"username": {} "username": {}
} }
}, },
"keysCached": "Keys are cached",
"@keysCached": {
"type": "text",
"placeholders": {}
},
"keysMissing": "Keys are missing",
"@keysMissing": {
"type": "text",
"placeholders": {}
},
"kicked": "{username} kicked {targetName}", "kicked": "{username} kicked {targetName}",
"@kicked": { "@kicked": {
"type": "text", "type": "text",
@ -788,6 +870,21 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"newVerificationRequest": "New verification request!",
"@newVerificationRequest": {
"type": "text",
"placeholders": {}
},
"noCrossSignBootstrap": "Fluffychat currently does not support enabling Cross-Signing. Please enable it from within Riot.",
"@noCrossSignBootstrap": {
"type": "text",
"placeholders": {}
},
"noMegolmBootstrap": "Fluffychat currently does not support enabling Online Key Backup. Please enable it from within Riot.",
"@noMegolmBootstrap": {
"type": "text",
"placeholders": {}
},
"It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/": "It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/", "It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/": "It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/",
"@It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/": { "@It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/": {
"type": "text", "type": "text",
@ -830,6 +927,16 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"onlineKeyBackupDisabled": "Online Key Backup is disabled",
"@onlineKeyBackupDisabled": {
"type": "text",
"placeholders": {}
},
"onlineKeyBackupEnabled": "Online Key Backup is enabled",
"@onlineKeyBackupEnabled": {
"type": "text",
"placeholders": {}
},
"Oops something went wrong...": "Oops something went wrong...", "Oops something went wrong...": "Oops something went wrong...",
"@Oops something went wrong...": { "@Oops something went wrong...": {
"type": "text", "type": "text",
@ -855,6 +962,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"passphraseOrKey": "passphrase or recovery key",
"@passphraseOrKey": {
"type": "text",
"placeholders": {}
},
"Password": "Password", "Password": "Password",
"@Password": { "@Password": {
"type": "text", "type": "text",
@ -897,6 +1009,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"Reject": "Reject",
"@Reject": {
"type": "text",
"placeholders": {}
},
"Rejoin": "Rejoin", "Rejoin": "Rejoin",
"@Rejoin": { "@Rejoin": {
"type": "text", "type": "text",
@ -978,6 +1095,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"Room has been upgraded": "Room has been upgraded",
"@Room has been upgraded": {
"type": "text",
"placeholders": {}
},
"Saturday": "Saturday", "Saturday": "Saturday",
"@Saturday": { "@Saturday": {
"type": "text", "type": "text",
@ -1083,6 +1205,11 @@
"username": {} "username": {}
} }
}, },
"sessionVerified": "Session is verified",
"@sessionVerified": {
"type": "text",
"placeholders": {}
},
"Set a profile picture": "Set a profile picture", "Set a profile picture": "Set a profile picture",
"@Set a profile picture": { "@Set a profile picture": {
"type": "text", "type": "text",
@ -1113,6 +1240,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"Skip": "Skip",
"@Skip": {
"type": "text",
"placeholders": {}
},
"Change your style": "Change your style", "Change your style": "Change your style",
"@Change your style": { "@Change your style": {
"type": "text", "type": "text",
@ -1153,6 +1285,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"Submit": "Submit",
"@Submit": {
"type": "text",
"placeholders": {}
},
"Sunday": "Sunday", "Sunday": "Sunday",
"@Sunday": { "@Sunday": {
"type": "text", "type": "text",
@ -1168,6 +1305,16 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"They Don't Match": "They Don't Match",
"@They Don't Match": {
"type": "text",
"placeholders": {}
},
"They Match": "They Match",
"@They Match": {
"type": "text",
"placeholders": {}
},
"This room has been archived.": "This room has been archived.", "This room has been archived.": "This room has been archived.",
"@This room has been archived.": { "@This room has been archived.": {
"type": "text", "type": "text",
@ -1212,6 +1359,11 @@
"targetName": {} "targetName": {}
} }
}, },
"Unblock Device": "Unblock Device",
"@Unblock Device": {
"type": "text",
"placeholders": {}
},
"Unmute chat": "Unmute chat", "Unmute chat": "Unmute chat",
"@Unmute chat": { "@Unmute chat": {
"type": "text", "type": "text",
@ -1227,6 +1379,11 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"unknownSessionVerify": "Unknown session, please verify",
"@unknownSessionVerify": {
"type": "text",
"placeholders": {}
},
"unknownEvent": "Unknown event '{type}'", "unknownEvent": "Unknown event '{type}'",
"@unknownEvent": { "@unknownEvent": {
"type": "text", "type": "text",
@ -1297,6 +1454,36 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"verifyManual": "Verify Manually",
"@verifyManual": {
"type": "text",
"placeholders": {}
},
"verifiedSession": "Successfully verified session!",
"@verifiedSession": {
"type": "text",
"placeholders": {}
},
"verifyStart": "Start Verification",
"@verifyStart": {
"type": "text",
"placeholders": {}
},
"verifySuccess": "You successfully verified!",
"@verifySuccess": {
"type": "text",
"placeholders": {}
},
"verifyTitle": "Verifying other account",
"@verifyTitle": {
"type": "text",
"placeholders": {}
},
"Verify User": "Verify User",
"@Verify User": {
"type": "text",
"placeholders": {}
},
"Video call": "Video call", "Video call": "Video call",
"@Video call": { "@Video call": {
"type": "text", "type": "text",
@ -1322,6 +1509,21 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"waitingPartnerAcceptRequest": "Waiting for partner to accept the request...",
"@waitingPartnerAcceptRequest": {
"type": "text",
"placeholders": {}
},
"waitingPartnerEmoji": "Waiting for partner to accept the emoji...",
"@waitingPartnerEmoji": {
"type": "text",
"placeholders": {}
},
"waitingPartnerNumbers": "Waiting for partner to accept the numbers...",
"@waitingPartnerNumbers": {
"type": "text",
"placeholders": {}
},
"Wallpaper": "Wallpaper", "Wallpaper": "Wallpaper",
"@Wallpaper": { "@Wallpaper": {
"type": "text", "type": "text",
@ -1387,4 +1589,4 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
} }
} }

View file

@ -46,6 +46,8 @@ class L10n extends MatrixLocalizations {
String get about => Intl.message("About"); String get about => Intl.message("About");
String get accept => Intl.message("Accept");
String acceptedTheInvitation(String username) => Intl.message( String acceptedTheInvitation(String username) => Intl.message(
"$username accepted the invitation", "$username accepted the invitation",
name: "acceptedTheInvitation", name: "acceptedTheInvitation",
@ -81,6 +83,22 @@ class L10n extends MatrixLocalizations {
String get areYouSure => Intl.message("Are you sure?"); String get areYouSure => Intl.message("Are you sure?");
String get askSSSSCache => Intl.message(
"Please enter your secure store passphrase or recovery key to cache the keys.",
name: "askSSSSCache");
String get askSSSSSign => Intl.message(
"To be able to sign the other person, please enter your secure store passphrase or recovery key.",
name: "askSSSSSign");
String get askSSSSVerify => Intl.message(
"Please enter your secure store passphrase or recovery key to verify your session.",
name: "askSSSSVerify");
String askVerificationRequest(String username) =>
Intl.message("Accept this verification request from $username?",
name: "askVerificationRequest", args: [username]);
String get authentication => Intl.message("Authentication"); String get authentication => Intl.message("Authentication");
String get avatarHasBeenChanged => Intl.message("Avatar has been changed"); String get avatarHasBeenChanged => Intl.message("Avatar has been changed");
@ -95,12 +113,17 @@ class L10n extends MatrixLocalizations {
args: [username, targetName], args: [username, targetName],
); );
String get blockDevice => Intl.message("Block Device");
String byDefaultYouWillBeConnectedTo(String homeserver) => Intl.message( String byDefaultYouWillBeConnectedTo(String homeserver) => Intl.message(
'By default you will be connected to $homeserver', 'By default you will be connected to $homeserver',
name: 'byDefaultYouWillBeConnectedTo', name: 'byDefaultYouWillBeConnectedTo',
args: [homeserver], args: [homeserver],
); );
String get cachedKeys =>
Intl.message("Successfully cached keys!", name: "cachedKeys");
String get cancel => Intl.message("Cancel"); String get cancel => Intl.message("Cancel");
String changedTheChatAvatar(String username) => Intl.message( String changedTheChatAvatar(String username) => Intl.message(
@ -216,6 +239,14 @@ class L10n extends MatrixLocalizations {
String get close => Intl.message("Close"); String get close => Intl.message("Close");
String get compareEmojiMatch => Intl.message(
"Compare and make sure the following emoji match those of the other device:",
name: "compareEmojiMatch");
String get compareNumbersMatch => Intl.message(
"Compare and make sure the following numbers match those of the other device:",
name: "compareNumbersMatch");
String get confirm => Intl.message("Confirm"); String get confirm => Intl.message("Confirm");
String get connect => Intl.message('Connect'); String get connect => Intl.message('Connect');
@ -261,6 +292,12 @@ class L10n extends MatrixLocalizations {
String get createNewGroup => Intl.message("Create new group"); String get createNewGroup => Intl.message("Create new group");
String get crossSigningDisabled =>
Intl.message("Cross-Signing is disabled", name: "crossSigningDisabled");
String get crossSigningEnabled =>
Intl.message("Cross-Signing is enabled", name: "crossSigningEnabled");
String get currentlyActive => Intl.message('Currently active'); String get currentlyActive => Intl.message('Currently active');
String dateAndTimeOfDay(String date, String timeOfDay) => Intl.message( String dateAndTimeOfDay(String date, String timeOfDay) => Intl.message(
@ -319,6 +356,8 @@ class L10n extends MatrixLocalizations {
String get enableEncryptionWarning => Intl.message( String get enableEncryptionWarning => Intl.message(
"You won't be able to disable the encryption anymore. Are you sure?"); "You won't be able to disable the encryption anymore. Are you sure?");
String get encryption => Intl.message("Encryption");
String get encryptionAlgorithm => Intl.message("Encryption algorithm"); String get encryptionAlgorithm => Intl.message("Encryption algorithm");
String get encryptionNotEnabled => Intl.message("Encryption is not enabled"); String get encryptionNotEnabled => Intl.message("Encryption is not enabled");
@ -381,6 +420,10 @@ class L10n extends MatrixLocalizations {
String get identity => Intl.message("Identity"); String get identity => Intl.message("Identity");
String get incorrectPassphraseOrKey =>
Intl.message("Incorrect passphrase or recovery key",
name: "incorrectPassphraseOrKey");
String get inviteContact => Intl.message("Invite contact"); String get inviteContact => Intl.message("Invite contact");
String inviteContactToGroup(String groupName) => Intl.message( String inviteContactToGroup(String groupName) => Intl.message(
@ -405,6 +448,10 @@ class L10n extends MatrixLocalizations {
String get invitedUsersOnly => Intl.message("Invited users only"); String get invitedUsersOnly => Intl.message("Invited users only");
String get isDeviceKeyCorrect =>
Intl.message("Is the following device key correct?",
name: "isDeviceKeyCorrect");
String get isTyping => Intl.message("is typing..."); String get isTyping => Intl.message("is typing...");
String get editJitsiInstance => Intl.message('Edit Jitsi instance'); String get editJitsiInstance => Intl.message('Edit Jitsi instance');
@ -415,6 +462,11 @@ class L10n extends MatrixLocalizations {
args: [username], args: [username],
); );
String get keysCached => Intl.message("Keys are cached", name: "keysCached");
String get keysMissing =>
Intl.message("Keys are missing", name: "keysMissing");
String kicked(String username, String targetName) => Intl.message( String kicked(String username, String targetName) => Intl.message(
"$username kicked $targetName", "$username kicked $targetName",
name: "kicked", name: "kicked",
@ -493,6 +545,17 @@ class L10n extends MatrixLocalizations {
String get newPrivateChat => Intl.message("New private chat"); String get newPrivateChat => Intl.message("New private chat");
String get newVerificationRequest =>
Intl.message("New verification request!", name: "newVerificationRequest");
String get noCrossSignBootstrap => Intl.message(
"Fluffychat currently does not support enabling Cross-Signing. Please enable it from within Riot.",
name: "noCrossSignBootstrap");
String get noMegolmBootstrap => Intl.message(
"Fluffychat currently does not support enabling Online Key Backup. Please enable it from within Riot.",
name: "noMegolmBootstrap");
String get noGoogleServicesWarning => Intl.message( String get noGoogleServicesWarning => Intl.message(
"It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/"); "It seems that you have no google services on your phone. That's a good decision for your privacy! To receive push notifications in FluffyChat we recommend using microG: https://microg.org/");
@ -511,6 +574,14 @@ class L10n extends MatrixLocalizations {
String get ok => Intl.message('ok'); String get ok => Intl.message('ok');
String get onlineKeyBackupDisabled =>
Intl.message("Online Key Backup is disabled",
name: "onlineKeyBackupDisabled");
String get onlineKeyBackupEnabled =>
Intl.message("Online Key Backup is enabled",
name: "onlineKeyBackupEnabled");
String get oopsSomethingWentWrong => String get oopsSomethingWentWrong =>
Intl.message("Oops something went wrong..."); Intl.message("Oops something went wrong...");
@ -523,6 +594,9 @@ class L10n extends MatrixLocalizations {
String get participatingUserDevices => String get participatingUserDevices =>
Intl.message("Participating user devices"); Intl.message("Participating user devices");
String get passphraseOrKey =>
Intl.message("passphrase or recovery key", name: "passphraseOrKey");
String get password => Intl.message("Password"); String get password => Intl.message("Password");
String get pickImage => Intl.message('Pick image'); String get pickImage => Intl.message('Pick image');
@ -546,6 +620,8 @@ class L10n extends MatrixLocalizations {
String get publicRooms => Intl.message("Public Rooms"); String get publicRooms => Intl.message("Public Rooms");
String get reject => Intl.message("Reject");
String get rejoin => Intl.message("Rejoin"); String get rejoin => Intl.message("Rejoin");
String get renderRichContent => Intl.message("Render rich message content"); String get renderRichContent => Intl.message("Render rich message content");
@ -662,6 +738,9 @@ class L10n extends MatrixLocalizations {
args: [username], args: [username],
); );
String get sessionVerified =>
Intl.message("Session is verified", name: "sessionVerified");
String get setAProfilePicture => Intl.message("Set a profile picture"); String get setAProfilePicture => Intl.message("Set a profile picture");
String get setGroupDescription => Intl.message("Set group description"); String get setGroupDescription => Intl.message("Set group description");
@ -674,6 +753,8 @@ class L10n extends MatrixLocalizations {
String get signUp => Intl.message("Sign up"); String get signUp => Intl.message("Sign up");
String get skip => Intl.message("Skip");
String get changeTheme => Intl.message("Change your style"); String get changeTheme => Intl.message("Change your style");
String get systemTheme => Intl.message("System"); String get systemTheme => Intl.message("System");
@ -690,12 +771,18 @@ class L10n extends MatrixLocalizations {
String get startYourFirstChat => Intl.message("Start your first chat :-)"); String get startYourFirstChat => Intl.message("Start your first chat :-)");
String get submit => Intl.message("Submit");
String get sunday => Intl.message("Sunday"); String get sunday => Intl.message("Sunday");
String get donate => Intl.message("Donate"); String get donate => Intl.message("Donate");
String get tapToShowMenu => Intl.message("Tap to show menu"); String get tapToShowMenu => Intl.message("Tap to show menu");
String get theyDontMatch => Intl.message("They Don't Match");
String get theyMatch => Intl.message("They Match");
String get thisRoomHasBeenArchived => String get thisRoomHasBeenArchived =>
Intl.message("This room has been archived."); Intl.message("This room has been archived.");
@ -726,6 +813,8 @@ class L10n extends MatrixLocalizations {
args: [username, targetName], args: [username, targetName],
); );
String get unblockDevice => Intl.message("Unblock Device");
String get unmuteChat => Intl.message('Unmute chat'); String get unmuteChat => Intl.message('Unmute chat');
String get unknownDevice => Intl.message("Unknown device"); String get unknownDevice => Intl.message("Unknown device");
@ -733,6 +822,10 @@ class L10n extends MatrixLocalizations {
String get unknownEncryptionAlgorithm => String get unknownEncryptionAlgorithm =>
Intl.message("Unknown encryption algorithm"); Intl.message("Unknown encryption algorithm");
String get unknownSessionVerify =>
Intl.message("Unknown session, please verify",
name: "unknownSessionVerify");
String unknownEvent(String type) => Intl.message( String unknownEvent(String type) => Intl.message(
"Unknown event '$type'", "Unknown event '$type'",
name: "unknownEvent", name: "unknownEvent",
@ -787,6 +880,23 @@ class L10n extends MatrixLocalizations {
String get verify => Intl.message("Verify"); String get verify => Intl.message("Verify");
String get verifyManual =>
Intl.message("Verify Manually", name: "verifyManual");
String get verifiedSession =>
Intl.message("Successfully verified session!", name: "verifiedSession");
String get verifyStart =>
Intl.message("Start Verification", name: "verifyStart");
String get verifySuccess =>
Intl.message("You successfully verified!", name: "verifySuccess");
String get verifyTitle =>
Intl.message("Verifying other account", name: "verifyTitle");
String get verifyUser => Intl.message("Verify User");
String get videoCall => Intl.message('Video call'); String get videoCall => Intl.message('Video call');
String get visibleForAllParticipants => String get visibleForAllParticipants =>
@ -799,6 +909,18 @@ class L10n extends MatrixLocalizations {
String get voiceMessage => Intl.message("Voice message"); String get voiceMessage => Intl.message("Voice message");
String get waitingPartnerAcceptRequest =>
Intl.message("Waiting for partner to accept the request...",
name: "waitingPartnerAcceptRequest");
String get waitingPartnerEmoji =>
Intl.message("Waiting for partner to accept the emoji...",
name: "waitingPartnerEmoji");
String get waitingPartnerNumbers =>
Intl.message("Waiting for partner to accept the numbers...",
name: "waitingPartnerNumbers");
String get wallpaper => Intl.message("Wallpaper"); String get wallpaper => Intl.message("Wallpaper");
String get warningEncryptionInBeta => Intl.message( String get warningEncryptionInBeta => Intl.message(

View file

@ -23,6 +23,8 @@ class MessageLookup extends MessageLookupByLibrary {
static m1(username) => "${username} activated end to end encryption"; static m1(username) => "${username} activated end to end encryption";
static m60(username) => "Accept this verification request from ${username}?";
static m2(username, targetName) => "${username} banned ${targetName}"; static m2(username, targetName) => "${username} banned ${targetName}";
static m3(homeserver) => "By default you will be connected to ${homeserver}"; static m3(homeserver) => "By default you will be connected to ${homeserver}";
@ -157,6 +159,7 @@ class MessageLookup extends MessageLookupByLibrary {
"(Optional) Group name": "(Optional) Group name":
MessageLookupByLibrary.simpleMessage("(Optional) Group name"), MessageLookupByLibrary.simpleMessage("(Optional) Group name"),
"About": MessageLookupByLibrary.simpleMessage("About"), "About": MessageLookupByLibrary.simpleMessage("About"),
"Accept": MessageLookupByLibrary.simpleMessage("Accept"),
"Account": MessageLookupByLibrary.simpleMessage("Account"), "Account": MessageLookupByLibrary.simpleMessage("Account"),
"Account informations": "Account informations":
MessageLookupByLibrary.simpleMessage("Account informations"), MessageLookupByLibrary.simpleMessage("Account informations"),
@ -178,6 +181,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Avatar has been changed"), MessageLookupByLibrary.simpleMessage("Avatar has been changed"),
"Ban from chat": MessageLookupByLibrary.simpleMessage("Ban from chat"), "Ban from chat": MessageLookupByLibrary.simpleMessage("Ban from chat"),
"Banned": MessageLookupByLibrary.simpleMessage("Banned"), "Banned": MessageLookupByLibrary.simpleMessage("Banned"),
"Block Device": MessageLookupByLibrary.simpleMessage("Block Device"),
"Cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "Cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
"Change the homeserver": "Change the homeserver":
MessageLookupByLibrary.simpleMessage("Change the homeserver"), MessageLookupByLibrary.simpleMessage("Change the homeserver"),
@ -242,6 +246,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Emote shortcode": "Emote shortcode":
MessageLookupByLibrary.simpleMessage("Emote shortcode"), MessageLookupByLibrary.simpleMessage("Emote shortcode"),
"Empty chat": MessageLookupByLibrary.simpleMessage("Empty chat"), "Empty chat": MessageLookupByLibrary.simpleMessage("Empty chat"),
"Encryption": MessageLookupByLibrary.simpleMessage("Encryption"),
"Encryption algorithm": "Encryption algorithm":
MessageLookupByLibrary.simpleMessage("Encryption algorithm"), MessageLookupByLibrary.simpleMessage("Encryption algorithm"),
"Encryption is not enabled": "Encryption is not enabled":
@ -351,6 +356,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Please enter your username"), MessageLookupByLibrary.simpleMessage("Please enter your username"),
"Public Rooms": MessageLookupByLibrary.simpleMessage("Public Rooms"), "Public Rooms": MessageLookupByLibrary.simpleMessage("Public Rooms"),
"Recording": MessageLookupByLibrary.simpleMessage("Recording"), "Recording": MessageLookupByLibrary.simpleMessage("Recording"),
"Reject": MessageLookupByLibrary.simpleMessage("Reject"),
"Rejoin": MessageLookupByLibrary.simpleMessage("Rejoin"), "Rejoin": MessageLookupByLibrary.simpleMessage("Rejoin"),
"Remove": MessageLookupByLibrary.simpleMessage("Remove"), "Remove": MessageLookupByLibrary.simpleMessage("Remove"),
"Remove all other devices": "Remove all other devices":
@ -368,6 +374,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Request to read older messages"), "Request to read older messages"),
"Revoke all permissions": "Revoke all permissions":
MessageLookupByLibrary.simpleMessage("Revoke all permissions"), MessageLookupByLibrary.simpleMessage("Revoke all permissions"),
"Room has been upgraded":
MessageLookupByLibrary.simpleMessage("Room has been upgraded"),
"Saturday": MessageLookupByLibrary.simpleMessage("Saturday"), "Saturday": MessageLookupByLibrary.simpleMessage("Saturday"),
"Search for a chat": "Search for a chat":
MessageLookupByLibrary.simpleMessage("Search for a chat"), MessageLookupByLibrary.simpleMessage("Search for a chat"),
@ -388,9 +396,11 @@ class MessageLookup extends MessageLookupByLibrary {
"Settings": MessageLookupByLibrary.simpleMessage("Settings"), "Settings": MessageLookupByLibrary.simpleMessage("Settings"),
"Share": MessageLookupByLibrary.simpleMessage("Share"), "Share": MessageLookupByLibrary.simpleMessage("Share"),
"Sign up": MessageLookupByLibrary.simpleMessage("Sign up"), "Sign up": MessageLookupByLibrary.simpleMessage("Sign up"),
"Skip": MessageLookupByLibrary.simpleMessage("Skip"),
"Source code": MessageLookupByLibrary.simpleMessage("Source code"), "Source code": MessageLookupByLibrary.simpleMessage("Source code"),
"Start your first chat :-)": "Start your first chat :-)":
MessageLookupByLibrary.simpleMessage("Start your first chat :-)"), MessageLookupByLibrary.simpleMessage("Start your first chat :-)"),
"Submit": MessageLookupByLibrary.simpleMessage("Submit"),
"Sunday": MessageLookupByLibrary.simpleMessage("Sunday"), "Sunday": MessageLookupByLibrary.simpleMessage("Sunday"),
"System": MessageLookupByLibrary.simpleMessage("System"), "System": MessageLookupByLibrary.simpleMessage("System"),
"Tap to show menu": "Tap to show menu":
@ -398,12 +408,17 @@ class MessageLookup extends MessageLookupByLibrary {
"The encryption has been corrupted": "The encryption has been corrupted":
MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage(
"The encryption has been corrupted"), "The encryption has been corrupted"),
"They Don\'t Match":
MessageLookupByLibrary.simpleMessage("They Don\'t Match"),
"They Match": MessageLookupByLibrary.simpleMessage("They Match"),
"This room has been archived.": MessageLookupByLibrary.simpleMessage( "This room has been archived.": MessageLookupByLibrary.simpleMessage(
"This room has been archived."), "This room has been archived."),
"Thursday": MessageLookupByLibrary.simpleMessage("Thursday"), "Thursday": MessageLookupByLibrary.simpleMessage("Thursday"),
"Try to send again": "Try to send again":
MessageLookupByLibrary.simpleMessage("Try to send again"), MessageLookupByLibrary.simpleMessage("Try to send again"),
"Tuesday": MessageLookupByLibrary.simpleMessage("Tuesday"), "Tuesday": MessageLookupByLibrary.simpleMessage("Tuesday"),
"Unblock Device":
MessageLookupByLibrary.simpleMessage("Unblock Device"),
"Unknown device": "Unknown device":
MessageLookupByLibrary.simpleMessage("Unknown device"), MessageLookupByLibrary.simpleMessage("Unknown device"),
"Unknown encryption algorithm": MessageLookupByLibrary.simpleMessage( "Unknown encryption algorithm": MessageLookupByLibrary.simpleMessage(
@ -413,6 +428,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Use Amoled compatible colors?"), "Use Amoled compatible colors?"),
"Username": MessageLookupByLibrary.simpleMessage("Username"), "Username": MessageLookupByLibrary.simpleMessage("Username"),
"Verify": MessageLookupByLibrary.simpleMessage("Verify"), "Verify": MessageLookupByLibrary.simpleMessage("Verify"),
"Verify User": MessageLookupByLibrary.simpleMessage("Verify User"),
"Video call": MessageLookupByLibrary.simpleMessage("Video call"), "Video call": MessageLookupByLibrary.simpleMessage("Video call"),
"Visibility of the chat history": MessageLookupByLibrary.simpleMessage( "Visibility of the chat history": MessageLookupByLibrary.simpleMessage(
"Visibility of the chat history"), "Visibility of the chat history"),
@ -451,8 +467,17 @@ class MessageLookup extends MessageLookupByLibrary {
"acceptedTheInvitation": m0, "acceptedTheInvitation": m0,
"activatedEndToEndEncryption": m1, "activatedEndToEndEncryption": m1,
"alias": MessageLookupByLibrary.simpleMessage("alias"), "alias": MessageLookupByLibrary.simpleMessage("alias"),
"askSSSSCache": MessageLookupByLibrary.simpleMessage(
"Please enter your secure store passphrase or recovery key to cache the keys."),
"askSSSSSign": MessageLookupByLibrary.simpleMessage(
"To be able to sign the other person, please enter your secure store passphrase or recovery key."),
"askSSSSVerify": MessageLookupByLibrary.simpleMessage(
"Please enter your secure store passphrase or recovery key to verify your session."),
"askVerificationRequest": m60,
"bannedUser": m2, "bannedUser": m2,
"byDefaultYouWillBeConnectedTo": m3, "byDefaultYouWillBeConnectedTo": m3,
"cachedKeys":
MessageLookupByLibrary.simpleMessage("Successfully cached keys!"),
"changedTheChatAvatar": m4, "changedTheChatAvatar": m4,
"changedTheChatDescriptionTo": m5, "changedTheChatDescriptionTo": m5,
"changedTheChatNameTo": m6, "changedTheChatNameTo": m6,
@ -467,9 +492,17 @@ class MessageLookup extends MessageLookupByLibrary {
"changedTheProfileAvatar": m15, "changedTheProfileAvatar": m15,
"changedTheRoomAliases": m16, "changedTheRoomAliases": m16,
"changedTheRoomInvitationLink": m17, "changedTheRoomInvitationLink": m17,
"compareEmojiMatch": MessageLookupByLibrary.simpleMessage(
"Compare and make sure the following emoji match those of the other device:"),
"compareNumbersMatch": MessageLookupByLibrary.simpleMessage(
"Compare and make sure the following numbers match those of the other device:"),
"couldNotDecryptMessage": m18, "couldNotDecryptMessage": m18,
"countParticipants": m19, "countParticipants": m19,
"createdTheChat": m20, "createdTheChat": m20,
"crossSigningDisabled":
MessageLookupByLibrary.simpleMessage("Cross-Signing is disabled"),
"crossSigningEnabled":
MessageLookupByLibrary.simpleMessage("Cross-Signing is enabled"),
"dateAndTimeOfDay": m21, "dateAndTimeOfDay": m21,
"dateWithYear": m22, "dateWithYear": m22,
"dateWithoutYear": m23, "dateWithoutYear": m23,
@ -481,18 +514,36 @@ class MessageLookup extends MessageLookupByLibrary {
"You need to pick an emote shortcode and an image!"), "You need to pick an emote shortcode and an image!"),
"groupWith": m24, "groupWith": m24,
"hasWithdrawnTheInvitationFor": m25, "hasWithdrawnTheInvitationFor": m25,
"incorrectPassphraseOrKey": MessageLookupByLibrary.simpleMessage(
"Incorrect passphrase or recovery key"),
"inviteContactToGroup": m26, "inviteContactToGroup": m26,
"inviteText": m27, "inviteText": m27,
"invitedUser": m28, "invitedUser": m28,
"is typing...": MessageLookupByLibrary.simpleMessage("is typing..."), "is typing...": MessageLookupByLibrary.simpleMessage("is typing..."),
"isDeviceKeyCorrect": MessageLookupByLibrary.simpleMessage(
"Is the following device key correct?"),
"joinedTheChat": m29, "joinedTheChat": m29,
"keysCached": MessageLookupByLibrary.simpleMessage("Keys are cached"),
"keysMissing": MessageLookupByLibrary.simpleMessage("Keys are missing"),
"kicked": m30, "kicked": m30,
"kickedAndBanned": m31, "kickedAndBanned": m31,
"lastActiveAgo": m32, "lastActiveAgo": m32,
"loadCountMoreParticipants": m33, "loadCountMoreParticipants": m33,
"logInTo": m34, "logInTo": m34,
"newVerificationRequest":
MessageLookupByLibrary.simpleMessage("New verification request!"),
"noCrossSignBootstrap": MessageLookupByLibrary.simpleMessage(
"Fluffychat currently does not support enabling Cross-Signing. Please enable it from within Riot."),
"noMegolmBootstrap": MessageLookupByLibrary.simpleMessage(
"Fluffychat currently does not support enabling Online Key Backup. Please enable it from within Riot."),
"numberSelected": m35, "numberSelected": m35,
"ok": MessageLookupByLibrary.simpleMessage("ok"), "ok": MessageLookupByLibrary.simpleMessage("ok"),
"onlineKeyBackupDisabled": MessageLookupByLibrary.simpleMessage(
"Online Key Backup is disabled"),
"onlineKeyBackupEnabled": MessageLookupByLibrary.simpleMessage(
"Online Key Backup is enabled"),
"passphraseOrKey":
MessageLookupByLibrary.simpleMessage("passphrase or recovery key"),
"play": m36, "play": m36,
"redactedAnEvent": m37, "redactedAnEvent": m37,
"rejectedTheInvitation": m38, "rejectedTheInvitation": m38,
@ -505,11 +556,15 @@ class MessageLookup extends MessageLookupByLibrary {
"sentASticker": m45, "sentASticker": m45,
"sentAVideo": m46, "sentAVideo": m46,
"sentAnAudio": m47, "sentAnAudio": m47,
"sessionVerified":
MessageLookupByLibrary.simpleMessage("Session is verified"),
"sharedTheLocation": m48, "sharedTheLocation": m48,
"timeOfDay": m49, "timeOfDay": m49,
"title": MessageLookupByLibrary.simpleMessage("FluffyChat"), "title": MessageLookupByLibrary.simpleMessage("FluffyChat"),
"unbannedUser": m50, "unbannedUser": m50,
"unknownEvent": m51, "unknownEvent": m51,
"unknownSessionVerify": MessageLookupByLibrary.simpleMessage(
"Unknown session, please verify"),
"unreadChats": m52, "unreadChats": m52,
"unreadMessages": m53, "unreadMessages": m53,
"unreadMessagesInChats": m54, "unreadMessagesInChats": m54,
@ -517,6 +572,21 @@ class MessageLookup extends MessageLookupByLibrary {
"userAndUserAreTyping": m56, "userAndUserAreTyping": m56,
"userIsTyping": m57, "userIsTyping": m57,
"userLeftTheChat": m58, "userLeftTheChat": m58,
"userSentUnknownEvent": m59 "userSentUnknownEvent": m59,
"verifiedSession": MessageLookupByLibrary.simpleMessage(
"Successfully verified session!"),
"verifyManual": MessageLookupByLibrary.simpleMessage("Verify Manually"),
"verifyStart":
MessageLookupByLibrary.simpleMessage("Start Verification"),
"verifySuccess":
MessageLookupByLibrary.simpleMessage("You successfully verified!"),
"verifyTitle":
MessageLookupByLibrary.simpleMessage("Verifying other account"),
"waitingPartnerAcceptRequest": MessageLookupByLibrary.simpleMessage(
"Waiting for partner to accept the request..."),
"waitingPartnerEmoji": MessageLookupByLibrary.simpleMessage(
"Waiting for partner to accept the emoji..."),
"waitingPartnerNumbers": MessageLookupByLibrary.simpleMessage(
"Waiting for partner to accept the numbers...")
}; };
} }

View file

@ -127,7 +127,7 @@ Future<void> migrate(String clientName, Database db, Store store) async {
var sess = olm.Session(); var sess = olm.Session();
sess.unpickle(credentials['userID'], pickle); sess.unpickle(credentials['userID'], pickle);
await db.storeOlmSession( await db.storeOlmSession(
clientId, identKey, sess.session_id(), pickle); clientId, identKey, sess.session_id(), pickle, null);
sess?.free(); sess?.free();
} }
} }

View file

@ -16,10 +16,13 @@ extension RoomStatusExtension on Room {
if (directChatPresence.presence.currentlyActive == true) { if (directChatPresence.presence.currentlyActive == true) {
return L10n.of(context).currentlyActive; return L10n.of(context).currentlyActive;
} }
return L10n.of(context).lastActiveAgo( if (directChatPresence.presence.lastActiveAgo == null) {
DateTime.fromMillisecondsSinceEpoch( return L10n.of(context).lastSeenLongTimeAgo;
directChatPresence.presence.lastActiveAgo) }
.localizedTimeShort(context)); final time = DateTime.fromMillisecondsSinceEpoch(
DateTime.now().millisecondsSinceEpoch -
directChatPresence.presence.lastActiveAgo);
return L10n.of(context).lastActiveAgo(time.localizedTimeShort(context));
} }
return L10n.of(context).lastSeenLongTimeAgo; return L10n.of(context).lastSeenLongTimeAgo;
} }

View file

@ -1,4 +1,5 @@
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:fluffychat/components/adaptive_page_layout.dart'; import 'package:fluffychat/components/adaptive_page_layout.dart';
import 'package:fluffychat/components/avatar.dart'; import 'package:fluffychat/components/avatar.dart';
import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/components/matrix.dart';
@ -6,6 +7,9 @@ import 'package:fluffychat/utils/beautify_string_extension.dart';
import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/views/chat_list.dart'; import 'package:fluffychat/views/chat_list.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'key_verification.dart';
import '../utils/app_route.dart';
import '../components/dialogs/simple_dialogs.dart';
class ChatEncryptionSettingsView extends StatelessWidget { class ChatEncryptionSettingsView extends StatelessWidget {
final String id; final String id;
@ -33,6 +37,70 @@ class ChatEncryptionSettings extends StatefulWidget {
} }
class _ChatEncryptionSettingsState extends State<ChatEncryptionSettings> { class _ChatEncryptionSettingsState extends State<ChatEncryptionSettings> {
Future<void> onSelected(
BuildContext context, String action, DeviceKeys key) async {
final room = Matrix.of(context).client.getRoomById(widget.id);
final unblock = () async {
if (key.blocked) {
await key.setBlocked(false);
}
};
switch (action) {
case 'verify':
await unblock();
final req = key.startVerification();
req.onUpdate = () {
if (req.state == KeyVerificationState.done) {
setState(() => null);
}
};
await Navigator.of(context).push(
AppRoute.defaultRoute(
context,
KeyVerificationView(request: req),
),
);
break;
case 'verify_manual':
if (await SimpleDialogs(context).askConfirmation(
titleText: L10n.of(context).isDeviceKeyCorrect,
contentText: key.ed25519Key.beautified,
)) {
await unblock();
await key.setVerified(true);
setState(() => null);
}
break;
case 'verify_user':
await unblock();
final req =
await room.client.userDeviceKeys[key.userId].startVerification();
req.onUpdate = () {
if (req.state == KeyVerificationState.done) {
setState(() => null);
}
};
await Navigator.of(context).push(
AppRoute.defaultRoute(
context,
KeyVerificationView(request: req),
),
);
break;
case 'block':
if (key.directVerified) {
await key.setVerified(false);
}
await key.setBlocked(true);
setState(() => null);
break;
case 'unblock':
await unblock();
setState(() => null);
break;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final room = Matrix.of(context).client.getRoomById(widget.id); final room = Matrix.of(context).client.getRoomById(widget.id);
@ -68,59 +136,99 @@ class _ChatEncryptionSettingsState extends State<ChatEncryptionSettings> {
if (i == 0 || if (i == 0 ||
deviceKeys[i].userId != deviceKeys[i - 1].userId) deviceKeys[i].userId != deviceKeys[i - 1].userId)
Material( Material(
child: ListTile( child: PopupMenuButton(
leading: Avatar( onSelected: (action) =>
room onSelected(context, action, deviceKeys[i]),
itemBuilder: (c) {
var items = <PopupMenuEntry<String>>[];
if (room
.client
.userDeviceKeys[deviceKeys[i].userId]
.verified ==
UserVerifiedStatus.unknown &&
deviceKeys[i].userId != room.client.userID) {
items.add(PopupMenuItem(
child: Text(L10n.of(context).verifyUser),
value: 'verify_user',
));
}
return items;
},
child: ListTile(
leading: Avatar(
room
.getUserByMXIDSync(deviceKeys[i].userId)
.avatarUrl,
room
.getUserByMXIDSync(deviceKeys[i].userId)
.calcDisplayname(),
),
title: Text(room
.getUserByMXIDSync(deviceKeys[i].userId) .getUserByMXIDSync(deviceKeys[i].userId)
.avatarUrl, .calcDisplayname()),
room subtitle: Text(deviceKeys[i].userId),
.getUserByMXIDSync(deviceKeys[i].userId)
.calcDisplayname(),
), ),
title: Text(room
.getUserByMXIDSync(deviceKeys[i].userId)
.calcDisplayname()),
subtitle: Text(deviceKeys[i].userId),
), ),
elevation: 2, elevation: 2,
), ),
CheckboxListTile( PopupMenuButton(
title: Text( onSelected: (action) =>
"${deviceKeys[i].unsigned["device_display_name"] ?? L10n.of(context).unknownDevice} - ${deviceKeys[i].deviceId}", onSelected(context, action, deviceKeys[i]),
style: TextStyle( itemBuilder: (c) {
color: deviceKeys[i].blocked var items = <PopupMenuEntry<String>>[];
? Colors.red if (deviceKeys[i].blocked ||
: deviceKeys[i].verified !deviceKeys[i].verified) {
? Colors.green if (deviceKeys[i].userId == room.client.userID) {
: Colors.orange), items.add(PopupMenuItem(
), child: Text(L10n.of(context).verifyStart),
subtitle: Text( value: 'verify',
deviceKeys[i] ));
.keys['ed25519:${deviceKeys[i].deviceId}'] items.add(PopupMenuItem(
.beautified, child: Text(L10n.of(context).verifyManual),
style: TextStyle( value: 'verify_manual',
color: ));
Theme.of(context).textTheme.bodyText2.color), } else {
), items.add(PopupMenuItem(
value: deviceKeys[i].verified, child: Text(L10n.of(context).verifyUser),
onChanged: (bool newVal) { value: 'verify_user',
if (newVal == true) { ));
if (deviceKeys[i].blocked) {
deviceKeys[i]
.setBlocked(false, Matrix.of(context).client);
} }
deviceKeys[i]
.setVerified(true, Matrix.of(context).client);
} else {
if (deviceKeys[i].verified) {
deviceKeys[i].setVerified(
false, Matrix.of(context).client);
}
deviceKeys[i]
.setBlocked(true, Matrix.of(context).client);
} }
setState(() => null); if (deviceKeys[i].blocked) {
items.add(PopupMenuItem(
child: Text(L10n.of(context).unblockDevice),
value: 'unblock',
));
}
if (!deviceKeys[i].blocked) {
items.add(PopupMenuItem(
child: Text(L10n.of(context).blockDevice),
value: 'block',
));
}
return items;
}, },
child: ListTile(
title: Text(
"${deviceKeys[i].unsigned["device_display_name"] ?? L10n.of(context).unknownDevice} - ${deviceKeys[i].deviceId}",
style: TextStyle(
color: deviceKeys[i].blocked
? Colors.red
: deviceKeys[i].verified
? Colors.green
: Colors.orange),
),
subtitle: Text(
deviceKeys[i]
.keys['ed25519:${deviceKeys[i].deviceId}']
.beautified,
style: TextStyle(
color: Theme.of(context)
.textTheme
.bodyText2
.color),
),
),
), ),
], ],
), ),

View file

@ -0,0 +1,347 @@
import 'package:flutter/material.dart';
import 'package:famedlysdk/encryption.dart';
import 'package:famedlysdk/matrix_api.dart';
import 'chat_list.dart';
import '../components/adaptive_page_layout.dart';
import '../components/avatar.dart';
import '../components/dialogs/simple_dialogs.dart';
import '../l10n/l10n.dart';
class KeyVerificationView extends StatelessWidget {
final KeyVerification request;
KeyVerificationView({this.request});
@override
Widget build(BuildContext context) {
return AdaptivePageLayout(
primaryPage: FocusPage.SECOND,
firstScaffold: ChatList(),
secondScaffold: KeyVerificationPage(request: request),
);
}
}
class KeyVerificationPage extends StatefulWidget {
final KeyVerification request;
KeyVerificationPage({this.request});
@override
_KeyVerificationPageState createState() => _KeyVerificationPageState();
}
class _KeyVerificationPageState extends State<KeyVerificationPage> {
void Function() originalOnUpdate;
@override
void initState() {
originalOnUpdate = widget.request.onUpdate;
widget.request.onUpdate = () {
if (originalOnUpdate != null) {
originalOnUpdate();
}
setState(() => null);
};
widget.request.client.getProfileFromUserId(widget.request.userId).then((p) {
profile = p;
setState(() => null);
});
super.initState();
}
@override
void dispose() {
widget.request.onUpdate =
originalOnUpdate; // don't want to get updates anymore
if (![KeyVerificationState.error, KeyVerificationState.done]
.contains(widget.request.state)) {
widget.request.cancel('m.user');
}
super.dispose();
}
Profile profile;
@override
Widget build(BuildContext context) {
Widget body;
final buttons = <Widget>[];
switch (widget.request.state) {
case KeyVerificationState.askSSSS:
// prompt the user for their ssss passphrase / key
final textEditingController = TextEditingController();
String input;
final checkInput = () async {
if (input == null) {
return;
}
SimpleDialogs(context).showLoadingDialog(context);
// make sure the loading spinner shows before we test the keys
await Future.delayed(Duration(milliseconds: 100));
var valid = false;
try {
await widget.request.openSSSS(recoveryKey: input);
valid = true;
} catch (_) {
try {
await widget.request.openSSSS(passphrase: input);
valid = true;
} catch (_) {
valid = false;
}
}
await Navigator.of(context)?.pop();
if (!valid) {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).incorrectPassphraseOrKey,
);
}
};
body = Container(
margin: EdgeInsets.only(left: 8.0, right: 8.0),
child: Column(
children: <Widget>[
Text(L10n.of(context).askSSSSSign,
style: TextStyle(fontSize: 20)),
Container(height: 10),
TextField(
controller: textEditingController,
autofocus: false,
autocorrect: false,
onSubmitted: (s) {
input = s;
checkInput();
},
minLines: 1,
maxLines: 1,
obscureText: true,
decoration: InputDecoration(
hintText: L10n.of(context).passphraseOrKey,
prefixStyle: TextStyle(color: Theme.of(context).primaryColor),
suffixStyle: TextStyle(color: Theme.of(context).primaryColor),
border: OutlineInputBorder(),
),
),
],
mainAxisSize: MainAxisSize.min,
),
);
buttons.add(RaisedButton(
color: Theme.of(context).primaryColor,
elevation: 5,
textColor: Colors.white,
child: Text(L10n.of(context).submit),
onPressed: () {
input = textEditingController.text;
checkInput();
},
));
buttons.add(RaisedButton(
textColor: Theme.of(context).primaryColor,
elevation: 5,
color: Colors.white,
child: Text(L10n.of(context).skip),
onPressed: () => widget.request.openSSSS(skip: true),
));
break;
case KeyVerificationState.askAccept:
body = Container(
child: Text(
L10n.of(context).askVerificationRequest(widget.request.userId),
style: TextStyle(fontSize: 20)),
margin: EdgeInsets.only(left: 8.0, right: 8.0),
);
buttons.add(RaisedButton(
color: Theme.of(context).primaryColor,
elevation: 5,
textColor: Colors.white,
child: Text(L10n.of(context).accept),
onPressed: () => widget.request.acceptVerification(),
));
buttons.add(RaisedButton(
textColor: Theme.of(context).primaryColor,
elevation: 5,
color: Colors.white,
child: Text(L10n.of(context).reject),
onPressed: () {
widget.request.rejectVerification().then((_) {
Navigator.of(context).pop();
});
},
));
break;
case KeyVerificationState.waitingAccept:
body = Column(
children: <Widget>[
CircularProgressIndicator(),
Container(height: 10),
Text(L10n.of(context).waitingPartnerAcceptRequest),
],
mainAxisSize: MainAxisSize.min,
);
break;
case KeyVerificationState.askSas:
var emojiWidgets = <Widget>[];
// maybe add a button to switch between the two and only determine default
// view for if "emoji" is a present sasType or not?
String compareText;
if (widget.request.sasTypes.contains('emoji')) {
compareText = L10n.of(context).compareEmojiMatch;
emojiWidgets =
widget.request.sasEmojis.map((e) => _Emoji(e)).toList();
} else {
compareText = L10n.of(context).compareNumbersMatch;
final numbers = widget.request.sasNumbers;
emojiWidgets = <Widget>[
Text(numbers[0].toString(), style: TextStyle(fontSize: 40)),
Text('-', style: TextStyle(fontSize: 40)),
Text(numbers[1].toString(), style: TextStyle(fontSize: 40)),
Text('-', style: TextStyle(fontSize: 40)),
Text(numbers[2].toString(), style: TextStyle(fontSize: 40)),
];
}
body = Column(
children: <Widget>[
Container(
child: Text(compareText, style: TextStyle(fontSize: 20)),
margin: EdgeInsets.only(left: 8.0, right: 8.0),
),
Container(height: 10),
RichText(
text: TextSpan(
children:
emojiWidgets.map((w) => WidgetSpan(child: w)).toList(),
),
textAlign: TextAlign.center,
),
],
mainAxisSize: MainAxisSize.min,
);
buttons.add(RaisedButton(
color: Theme.of(context).primaryColor,
elevation: 5,
textColor: Colors.white,
child: Text(L10n.of(context).theyMatch),
onPressed: () => widget.request.acceptSas(),
));
buttons.add(RaisedButton(
textColor: Theme.of(context).primaryColor,
elevation: 5,
color: Colors.white,
child: Text(L10n.of(context).theyDontMatch),
onPressed: () => widget.request.rejectSas(),
));
break;
case KeyVerificationState.waitingSas:
var acceptText = widget.request.sasTypes.contains('emoji')
? L10n.of(context).waitingPartnerEmoji
: L10n.of(context).waitingPartnerNumbers;
body = Column(
children: <Widget>[
CircularProgressIndicator(),
Container(height: 10),
Text(acceptText),
],
mainAxisSize: MainAxisSize.min,
);
break;
case KeyVerificationState.done:
body = Column(
children: <Widget>[
Icon(Icons.check_circle, color: Colors.green, size: 200.0),
Container(height: 10),
Text(L10n.of(context).verifySuccess),
],
mainAxisSize: MainAxisSize.min,
);
buttons.add(RaisedButton(
color: Theme.of(context).primaryColor,
elevation: 5,
textColor: Colors.white,
child: Text(L10n.of(context).close),
onPressed: () => Navigator.of(context).pop(),
));
break;
case KeyVerificationState.error:
body = Column(
children: <Widget>[
Icon(Icons.cancel, color: Colors.red, size: 200.0),
Container(height: 10),
Text(
'Error ${widget.request.canceledCode}: ${widget.request.canceledReason}'),
],
mainAxisSize: MainAxisSize.min,
);
buttons.add(RaisedButton(
color: Theme.of(context).primaryColor,
elevation: 5,
textColor: Colors.white,
child: Text(L10n.of(context).close),
onPressed: () => Navigator.of(context).pop(),
));
break;
}
body ??= Text('ERROR: Unknown state ' + widget.request.state.toString());
final otherName = profile?.displayname ?? widget.request.userId;
var bottom;
if (widget.request.deviceId != null) {
final deviceName = widget
.request
.client
.userDeviceKeys[widget.request.userId]
?.deviceKeys[widget.request.deviceId]
?.deviceDisplayName ??
'';
bottom = PreferredSize(
child: Text('$deviceName (${widget.request.deviceId})',
style: TextStyle(color: Theme.of(context).textTheme.caption.color)),
preferredSize: Size(0.0, 20.0),
);
}
return Scaffold(
appBar: AppBar(
title: ListTile(
leading: Avatar(profile?.avatarUrl, otherName),
contentPadding: EdgeInsets.zero,
title: Text(L10n.of(context).verifyTitle),
isThreeLine: otherName != widget.request.userId,
subtitle: Column(
children: <Widget>[
Text(otherName),
if (otherName != widget.request.userId)
Text(widget.request.userId),
],
crossAxisAlignment: CrossAxisAlignment.start,
),
),
elevation: 0,
bottom: bottom,
),
extendBody: true,
extendBodyBehindAppBar: true,
body: Center(
child: body,
),
persistentFooterButtons: buttons.isEmpty ? null : buttons,
);
}
}
class _Emoji extends StatelessWidget {
final KeyVerificationEmoji emoji;
_Emoji(this.emoji);
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(emoji.emoji, style: TextStyle(fontSize: 50)),
Text(emoji.name),
Container(height: 10, width: 5),
],
);
}
}

View file

@ -38,6 +38,10 @@ class Settings extends StatefulWidget {
class _SettingsState extends State<Settings> { class _SettingsState extends State<Settings> {
Future<dynamic> profileFuture; Future<dynamic> profileFuture;
dynamic profile; dynamic profile;
Future<bool> crossSigningCachedFuture;
bool crossSigningCached;
Future<bool> megolmBackupCachedFuture;
bool megolmBackupCached;
void logoutAction(BuildContext context) async { void logoutAction(BuildContext context) async {
if (await SimpleDialogs(context).askConfirmation() == false) { if (await SimpleDialogs(context).askConfirmation() == false) {
@ -123,12 +127,65 @@ class _SettingsState extends State<Settings> {
setState(() => null); setState(() => null);
} }
Future<void> requestSSSSCache(BuildContext context) async {
final handle = Matrix.of(context).client.encryption.ssss.open();
final str = await SimpleDialogs(context).enterText(
titleText: L10n.of(context).askSSSSCache,
hintText: L10n.of(context).passphraseOrKey,
password: true,
);
if (str != null) {
SimpleDialogs(context).showLoadingDialog(context);
// make sure the loading spinner shows before we test the keys
await Future.delayed(Duration(milliseconds: 100));
var valid = false;
try {
handle.unlock(recoveryKey: str);
valid = true;
} catch (_) {
try {
handle.unlock(passphrase: str);
valid = true;
} catch (_) {
valid = false;
}
}
await Navigator.of(context)?.pop();
if (valid) {
await handle.maybeCacheAll();
await SimpleDialogs(context).inform(
contentText: L10n.of(context).cachedKeys,
);
setState(() {
crossSigningCachedFuture = null;
crossSigningCached = null;
megolmBackupCachedFuture = null;
megolmBackupCached = null;
});
} else {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).incorrectPassphraseOrKey,
);
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
profileFuture ??= client.ownProfile; profileFuture ??= client.ownProfile.then((p) {
profileFuture.then((p) {
if (mounted) setState(() => profile = p); if (mounted) setState(() => profile = p);
return p;
});
crossSigningCachedFuture ??=
client.encryption.crossSigning.isCached().then((c) {
if (mounted) setState(() => crossSigningCached = c);
return c;
});
megolmBackupCachedFuture ??=
client.encryption.keyManager.isCached().then((c) {
if (mounted) setState(() => megolmBackupCached = c);
return c;
}); });
return Scaffold( return Scaffold(
body: NestedScrollView( body: NestedScrollView(
@ -286,6 +343,110 @@ class _SettingsState extends State<Settings> {
onTap: () => logoutAction(context), onTap: () => logoutAction(context),
), ),
Divider(thickness: 1), Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).encryption,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
),
ListTile(
trailing: Icon(Icons.compare_arrows),
title: Text(client.encryption.crossSigning.enabled
? L10n.of(context).crossSigningEnabled
: L10n.of(context).crossSigningDisabled),
subtitle: client.encryption.crossSigning.enabled
? Text(client.isUnknownSession
? L10n.of(context).unknownSessionVerify
: L10n.of(context).sessionVerified +
', ' +
(crossSigningCached == null
? ''
: (crossSigningCached
? L10n.of(context).keysCached
: L10n.of(context).keysMissing)))
: null,
onTap: () async {
if (!client.encryption.crossSigning.enabled) {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).noCrossSignBootstrap,
);
return;
}
if (client.isUnknownSession) {
final str = await SimpleDialogs(context).enterText(
titleText: L10n.of(context).askSSSSVerify,
hintText: L10n.of(context).passphraseOrKey,
password: true,
);
if (str != null) {
SimpleDialogs(context).showLoadingDialog(context);
// make sure the loading spinner shows before we test the keys
await Future.delayed(Duration(milliseconds: 100));
var valid = false;
try {
await client.encryption.crossSigning
.selfSign(recoveryKey: str);
valid = true;
} catch (_) {
try {
await client.encryption.crossSigning
.selfSign(passphrase: str);
valid = true;
} catch (_) {
valid = false;
}
}
await Navigator.of(context)?.pop();
if (valid) {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).verifiedSession,
);
setState(() {
crossSigningCachedFuture = null;
crossSigningCached = null;
megolmBackupCachedFuture = null;
megolmBackupCached = null;
});
} else {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).incorrectPassphraseOrKey,
);
}
}
}
if (!(await client.encryption.crossSigning.isCached())) {
await requestSSSSCache(context);
}
},
),
ListTile(
trailing: Icon(Icons.wb_cloudy),
title: Text(client.encryption.keyManager.enabled
? L10n.of(context).onlineKeyBackupEnabled
: L10n.of(context).onlineKeyBackupDisabled),
subtitle: client.encryption.keyManager.enabled
? Text(megolmBackupCached == null
? ''
: (megolmBackupCached
? L10n.of(context).keysCached
: L10n.of(context).keysMissing))
: null,
onTap: () async {
if (!client.encryption.keyManager.enabled) {
await SimpleDialogs(context).inform(
contentText: L10n.of(context).noMegolmBootstrap,
);
return;
}
if (!(await client.encryption.keyManager.isCached())) {
await requestSSSSCache(context);
}
},
),
Divider(thickness: 1),
ListTile( ListTile(
title: Text( title: Text(
L10n.of(context).about, L10n.of(context).about,

View file

@ -29,6 +29,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.6.0" version: "1.6.0"
asn1lib:
dependency: transitive
description:
name: asn1lib
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.4"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -36,6 +43,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.4.1" version: "2.4.1"
base58check:
dependency: transitive
description:
name: base58check
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -127,6 +141,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.3" version: "1.3.3"
encrypt:
dependency: transitive
description:
name: encrypt
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.2"
encrypted_moor: encrypted_moor:
dependency: "direct main" dependency: "direct main"
description: description:
@ -136,19 +157,12 @@ packages:
url: "https://github.com/simolus3/moor.git" url: "https://github.com/simolus3/moor.git"
source: git source: git
version: "1.0.0" version: "1.0.0"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
famedlysdk: famedlysdk:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: b8c6decafc52cbf5c09288c6c6dde62b62ae978f ref: "8e2c8b0d1146e99e80ef5f5bf4b4c8e378772b06"
resolved-ref: b8c6decafc52cbf5c09288c6c6dde62b62ae978f resolved-ref: "8e2c8b0d1146e99e80ef5f5bf4b4c8e378772b06"
url: "https://gitlab.com/famedly/famedlysdk.git" url: "https://gitlab.com/famedly/famedlysdk.git"
source: git source: git
version: "0.0.1" version: "0.0.1"
@ -487,10 +501,10 @@ packages:
description: description:
path: "." path: "."
ref: "1.x.y" ref: "1.x.y"
resolved-ref: f66975bd1b5cb1865eba5efe6e3a392aa5e396a5 resolved-ref: "8e4fcccff7a2d4d0bd5142964db092bf45061905"
url: "https://gitlab.com/famedly/libraries/dart-olm.git" url: "https://gitlab.com/famedly/libraries/dart-olm.git"
source: git source: git
version: "1.1.1" version: "1.2.0"
open_file: open_file:
dependency: "direct main" dependency: "direct main"
description: description:
@ -512,13 +526,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.10" version: "1.0.10"
password_hash:
dependency: transitive
description:
name: password_hash
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
path: path:
dependency: transitive dependency: transitive
description: description:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.0" version: "1.6.4"
path_drawing: path_drawing:
dependency: transitive dependency: transitive
description: description:
@ -596,6 +617,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.2" version: "1.4.2"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
random_string: random_string:
dependency: "direct main" dependency: "direct main"
description: description:
@ -733,21 +761,21 @@ packages:
name: test name: test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.14.7" version: "1.14.4"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.16" version: "0.2.15"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.7" version: "0.3.4"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View file

@ -27,7 +27,7 @@ dependencies:
famedlysdk: famedlysdk:
git: git:
url: https://gitlab.com/famedly/famedlysdk.git url: https://gitlab.com/famedly/famedlysdk.git
ref: b8c6decafc52cbf5c09288c6c6dde62b62ae978f ref: 8e2c8b0d1146e99e80ef5f5bf4b4c8e378772b06
localstorage: ^3.0.1+4 localstorage: ^3.0.1+4
bubble: ^1.1.9+1 bubble: ^1.1.9+1