diff --git a/lib/app.dart b/lib/app.dart index b0825594..c460bede 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -113,7 +113,6 @@ class AppMainWidget extends StatefulWidget { class _AppMainWidgetState extends State { bool _isUserCreated = false; - bool _showDatabaseMigration = false; bool _showOnboarding = true; bool _isLoaded = false; bool _skipBackup = false; @@ -135,18 +134,12 @@ class _AppMainWidgetState extends State { // do not change in case twonly was already unlocked at some point _isTwonlyLocked = userService.currentUser.screenLockEnabled; } - if (userService.currentUser.appVersion < 62) { - _showDatabaseMigration = true; - } - } - - if (!_isUserCreated && !_showDatabaseMigration) { + } else { // This means the user is in the onboarding screen, so start with the Proof of Work. final (proof, disabled) = await apiService.getProofOfWork(); if (proof != null) { Log.info('Starting with proof of work calculation.'); - // Starting with the proof of work. _proofOfWork = ( calculatePoW(proof.prefix, proof.difficulty.toInt()), false, @@ -169,9 +162,7 @@ class _AppMainWidgetState extends State { late Widget child; - if (_showDatabaseMigration) { - child = const Center(child: Text('Please reinstall twonly.')); - } else if (_isUserCreated) { + if (_isUserCreated) { if (_isTwonlyLocked) { child = UnlockTwonlyView( callbackOnSuccess: () => setState(() { diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 81bc5d62..d93cacac 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -99,21 +99,6 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { )..where((t) => t.userId.equals(userid))).watchSingleOrNull(); } - Stream<(Contact, bool)?> watchContactAndVerificationState(int userid) { - final query = (select(contacts)..where((t) => t.userId.equals(userid))).join([ - leftOuterJoin( - keyVerifications, - keyVerifications.contactId.equalsExp(contacts.userId), - ), - ]); - return query - .map((row) => ( - row.readTable(contacts), - row.readTableOrNull(keyVerifications) != null, - )) - .watchSingleOrNull(); - } - Future> getAllContacts() { return select(contacts).get(); } diff --git a/lib/src/database/tables/contacts.table.dart b/lib/src/database/tables/contacts.table.dart index 315d6618..5dbf4770 100644 --- a/lib/src/database/tables/contacts.table.dart +++ b/lib/src/database/tables/contacts.table.dart @@ -47,18 +47,14 @@ enum VerificationType { @DataClassName('KeyVerification') class KeyVerifications extends Table { + IntColumn get verificationId => integer().autoIncrement()(); IntColumn get contactId => integer().references( Contacts, #userId, onDelete: KeyAction.cascade, )(); - TextColumn get type => textEnum()(); - DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); - - @override - Set get primaryKey => {contactId}; } @DataClassName('VerificationToken') diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 929d176c..6036d188 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -9041,6 +9041,21 @@ class $KeyVerificationsTable extends KeyVerifications final GeneratedDatabase attachedDatabase; final String? _alias; $KeyVerificationsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _verificationIdMeta = const VerificationMeta( + 'verificationId', + ); + @override + late final GeneratedColumn verificationId = GeneratedColumn( + 'verification_id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); static const VerificationMeta _contactIdMeta = const VerificationMeta( 'contactId', ); @@ -9050,7 +9065,7 @@ class $KeyVerificationsTable extends KeyVerifications aliasedName, false, type: DriftSqlType.int, - requiredDuringInsert: false, + requiredDuringInsert: true, defaultConstraints: GeneratedColumn.constraintIsAlways( 'REFERENCES contacts (user_id) ON DELETE CASCADE', ), @@ -9077,7 +9092,12 @@ class $KeyVerificationsTable extends KeyVerifications defaultValue: currentDateAndTime, ); @override - List get $columns => [contactId, type, createdAt]; + List get $columns => [ + verificationId, + contactId, + type, + createdAt, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -9090,11 +9110,22 @@ class $KeyVerificationsTable extends KeyVerifications }) { final context = VerificationContext(); final data = instance.toColumns(true); + if (data.containsKey('verification_id')) { + context.handle( + _verificationIdMeta, + verificationId.isAcceptableOrUnknown( + data['verification_id']!, + _verificationIdMeta, + ), + ); + } if (data.containsKey('contact_id')) { context.handle( _contactIdMeta, contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta), ); + } else if (isInserting) { + context.missing(_contactIdMeta); } if (data.containsKey('created_at')) { context.handle( @@ -9106,11 +9137,15 @@ class $KeyVerificationsTable extends KeyVerifications } @override - Set get $primaryKey => {contactId}; + Set get $primaryKey => {verificationId}; @override KeyVerification map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return KeyVerification( + verificationId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}verification_id'], + )!, contactId: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}contact_id'], @@ -9138,10 +9173,12 @@ class $KeyVerificationsTable extends KeyVerifications } class KeyVerification extends DataClass implements Insertable { + final int verificationId; final int contactId; final VerificationType type; final DateTime createdAt; const KeyVerification({ + required this.verificationId, required this.contactId, required this.type, required this.createdAt, @@ -9149,6 +9186,7 @@ class KeyVerification extends DataClass implements Insertable { @override Map toColumns(bool nullToAbsent) { final map = {}; + map['verification_id'] = Variable(verificationId); map['contact_id'] = Variable(contactId); { map['type'] = Variable( @@ -9161,6 +9199,7 @@ class KeyVerification extends DataClass implements Insertable { KeyVerificationsCompanion toCompanion(bool nullToAbsent) { return KeyVerificationsCompanion( + verificationId: Value(verificationId), contactId: Value(contactId), type: Value(type), createdAt: Value(createdAt), @@ -9173,6 +9212,7 @@ class KeyVerification extends DataClass implements Insertable { }) { serializer ??= driftRuntimeOptions.defaultSerializer; return KeyVerification( + verificationId: serializer.fromJson(json['verificationId']), contactId: serializer.fromJson(json['contactId']), type: $KeyVerificationsTable.$convertertype.fromJson( serializer.fromJson(json['type']), @@ -9184,6 +9224,7 @@ class KeyVerification extends DataClass implements Insertable { Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { + 'verificationId': serializer.toJson(verificationId), 'contactId': serializer.toJson(contactId), 'type': serializer.toJson( $KeyVerificationsTable.$convertertype.toJson(type), @@ -9193,16 +9234,21 @@ class KeyVerification extends DataClass implements Insertable { } KeyVerification copyWith({ + int? verificationId, int? contactId, VerificationType? type, DateTime? createdAt, }) => KeyVerification( + verificationId: verificationId ?? this.verificationId, contactId: contactId ?? this.contactId, type: type ?? this.type, createdAt: createdAt ?? this.createdAt, ); KeyVerification copyWithCompanion(KeyVerificationsCompanion data) { return KeyVerification( + verificationId: data.verificationId.present + ? data.verificationId.value + : this.verificationId, contactId: data.contactId.present ? data.contactId.value : this.contactId, type: data.type.present ? data.type.value : this.type, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, @@ -9212,6 +9258,7 @@ class KeyVerification extends DataClass implements Insertable { @override String toString() { return (StringBuffer('KeyVerification(') + ..write('verificationId: $verificationId, ') ..write('contactId: $contactId, ') ..write('type: $type, ') ..write('createdAt: $createdAt') @@ -9220,36 +9267,43 @@ class KeyVerification extends DataClass implements Insertable { } @override - int get hashCode => Object.hash(contactId, type, createdAt); + int get hashCode => Object.hash(verificationId, contactId, type, createdAt); @override bool operator ==(Object other) => identical(this, other) || (other is KeyVerification && + other.verificationId == this.verificationId && other.contactId == this.contactId && other.type == this.type && other.createdAt == this.createdAt); } class KeyVerificationsCompanion extends UpdateCompanion { + final Value verificationId; final Value contactId; final Value type; final Value createdAt; const KeyVerificationsCompanion({ + this.verificationId = const Value.absent(), this.contactId = const Value.absent(), this.type = const Value.absent(), this.createdAt = const Value.absent(), }); KeyVerificationsCompanion.insert({ - this.contactId = const Value.absent(), + this.verificationId = const Value.absent(), + required int contactId, required VerificationType type, this.createdAt = const Value.absent(), - }) : type = Value(type); + }) : contactId = Value(contactId), + type = Value(type); static Insertable custom({ + Expression? verificationId, Expression? contactId, Expression? type, Expression? createdAt, }) { return RawValuesInsertable({ + if (verificationId != null) 'verification_id': verificationId, if (contactId != null) 'contact_id': contactId, if (type != null) 'type': type, if (createdAt != null) 'created_at': createdAt, @@ -9257,11 +9311,13 @@ class KeyVerificationsCompanion extends UpdateCompanion { } KeyVerificationsCompanion copyWith({ + Value? verificationId, Value? contactId, Value? type, Value? createdAt, }) { return KeyVerificationsCompanion( + verificationId: verificationId ?? this.verificationId, contactId: contactId ?? this.contactId, type: type ?? this.type, createdAt: createdAt ?? this.createdAt, @@ -9271,6 +9327,9 @@ class KeyVerificationsCompanion extends UpdateCompanion { @override Map toColumns(bool nullToAbsent) { final map = {}; + if (verificationId.present) { + map['verification_id'] = Variable(verificationId.value); + } if (contactId.present) { map['contact_id'] = Variable(contactId.value); } @@ -9288,6 +9347,7 @@ class KeyVerificationsCompanion extends UpdateCompanion { @override String toString() { return (StringBuffer('KeyVerificationsCompanion(') + ..write('verificationId: $verificationId, ') ..write('contactId: $contactId, ') ..write('type: $type, ') ..write('createdAt: $createdAt') @@ -19473,12 +19533,14 @@ typedef $$GroupHistoriesTableProcessedTableManager = >; typedef $$KeyVerificationsTableCreateCompanionBuilder = KeyVerificationsCompanion Function({ - Value contactId, + Value verificationId, + required int contactId, required VerificationType type, Value createdAt, }); typedef $$KeyVerificationsTableUpdateCompanionBuilder = KeyVerificationsCompanion Function({ + Value verificationId, Value contactId, Value type, Value createdAt, @@ -19522,6 +19584,11 @@ class $$KeyVerificationsTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + ColumnFilters get verificationId => $composableBuilder( + column: $table.verificationId, + builder: (column) => ColumnFilters(column), + ); + ColumnWithTypeConverterFilters get type => $composableBuilder( column: $table.type, @@ -19566,6 +19633,11 @@ class $$KeyVerificationsTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + ColumnOrderings get verificationId => $composableBuilder( + column: $table.verificationId, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get type => $composableBuilder( column: $table.type, builder: (column) => ColumnOrderings(column), @@ -19609,6 +19681,11 @@ class $$KeyVerificationsTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); + GeneratedColumn get verificationId => $composableBuilder( + column: $table.verificationId, + builder: (column) => column, + ); + GeneratedColumnWithTypeConverter get type => $composableBuilder(column: $table.type, builder: (column) => column); @@ -19669,20 +19746,24 @@ class $$KeyVerificationsTableTableManager $$KeyVerificationsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ + Value verificationId = const Value.absent(), Value contactId = const Value.absent(), Value type = const Value.absent(), Value createdAt = const Value.absent(), }) => KeyVerificationsCompanion( + verificationId: verificationId, contactId: contactId, type: type, createdAt: createdAt, ), createCompanionCallback: ({ - Value contactId = const Value.absent(), + Value verificationId = const Value.absent(), + required int contactId, required VerificationType type, Value createdAt = const Value.absent(), }) => KeyVerificationsCompanion.insert( + verificationId: verificationId, contactId: contactId, type: type, createdAt: createdAt, diff --git a/lib/src/services/intent/links.intent.dart b/lib/src/services/intent/links.intent.dart index 5c1360b4..193bb85d 100644 --- a/lib/src/services/intent/links.intent.dart +++ b/lib/src/services/intent/links.intent.dart @@ -16,6 +16,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/qr.utils.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/visual/views/chats/add_new_user.view.dart'; @@ -25,6 +26,41 @@ Future handleIntentUrl(BuildContext context, Uri uri) async { if (uri.host != 'me.twonly.eu') return false; if (uri.hasEmptyPath) return false; + // Check if this is the QR code link which was + // therefore scanned with the system camera + + if (uri.toString().startsWith(QrCodeUtils.linkPrefix)) { + final result = await QrCodeUtils.handleQrCodeLink(uri.toString()); + + if (!context.mounted) return false; + + if (result != null) { + final (profile, contact, verificationOk) = result; + + if (profile.username == userService.currentUser.username) { + await context.push(Routes.settingsPublicProfile); + return true; + } + + if (contact != null) { + if (verificationOk) { + await context.push(Routes.profileContact(contact.userId)); + } else { + await _pubKeysDoNotMatch(context, contact.username); + } + } else { + await context.navPush( + AddNewUserView( + username: profile.username, + publicKey: Uint8List.fromList(profile.publicIdentityKey), + ), + ); + } + } + + return true; + } + final publicKey = uri.hasFragment ? uri.fragment : null; final userPaths = uri.path.split('/'); if (userPaths.length != 2) return false; @@ -40,25 +76,26 @@ Future handleIntentUrl(BuildContext context, Uri uri) async { Log.info( 'Opened via deep link!: username = $username public_key = ${uri.fragment}', ); + final contacts = await twonlyDB.contactsDao.getContactsByUsername(username); + if (!context.mounted) return true; + if (contacts.isEmpty) { - if (!context.mounted) return true; + // User does not yet exists, making a request... Uint8List? publicKeyBytes; if (publicKey != null) { publicKeyBytes = base64Url.decode(publicKey); } - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return AddNewUserView( - username: username, - publicKey: publicKeyBytes, - ); - }, + await context.navPush( + AddNewUserView( + username: username, + publicKey: publicKeyBytes, ), ); - } else if (publicKey != null) { + return true; + } + + if (publicKey != null) { try { final contact = contacts.first; final storedPublicKey = await getPublicKeyFromContact(contact.userId); @@ -85,20 +122,25 @@ Future handleIntentUrl(BuildContext context, Uri uri) async { await context.push(Routes.profileContact(contact.userId)); } } else { - await showAlertDialog( - context, - context.lang.couldNotVerifyUsername(contact.username), - context.lang.linkPubkeyDoesNotMatch, - customCancel: '', - ); + await _pubKeysDoNotMatch(context, contact.username); } } catch (e) { Log.warn(e); } } + return true; } +Future _pubKeysDoNotMatch(BuildContext context, String username) async { + await showAlertDialog( + context, + context.lang.couldNotVerifyUsername(username), + context.lang.linkPubkeyDoesNotMatch, + customCancel: '', + ); +} + Future handleIntentMediaFile( BuildContext context, String filePath, diff --git a/lib/src/utils/qr.dart b/lib/src/utils/qr.utils.dart similarity index 100% rename from lib/src/utils/qr.dart rename to lib/src/utils/qr.utils.dart diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart index 32f59a8f..ccc4c034 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -21,7 +21,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/qr.dart'; +import 'package:twonly/src/utils/qr.utils.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart'; import 'package:twonly/src/visual/helpers/screenshot.helper.dart'; @@ -377,7 +377,7 @@ class _CameraPreviewViewState extends State { // shouldReturn is null when the user used the back button if (shouldReturn != null && shouldReturn) { if (widget.sendToGroup == null) { - globalUpdateOfHomeViewPageIndex(0); + HomeViewState.streamHomeViewPageIndex.add(0); } else if (mounted) { Navigator.pop(context); } diff --git a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart index 2fac41c4..9504306b 100644 --- a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart @@ -16,7 +16,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/qr.dart'; +import 'package:twonly/src/utils/qr.utils.dart'; import 'package:twonly/src/visual/helpers/screenshot.helper.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/face_filters.dart'; @@ -45,7 +45,7 @@ class MainCameraController { bool initCameraStarted = true; Map contactsVerified = {}; Map scannedNewProfiles = {}; - Set _handledProfileLinks = {}; + final Set _handledProfileLinks = {}; String? scannedUrl; GlobalKey zoomButtonKey = GlobalKey(); GlobalKey cameraPreviewKey = GlobalKey(); @@ -381,6 +381,7 @@ class MainCameraController { ); } } + continue; } if (link.startsWith('http://') || link.startsWith('https://')) { diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index 81d0981e..8fd6016f 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:drift/drift.dart'; -import 'package:intl/intl.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; @@ -35,23 +35,21 @@ class _ContactViewState extends State { List _memberOfGroups = []; List _keyVerifications = []; - late StreamSubscription<(Contact, bool)?> _contactSub; + late StreamSubscription _contactSub; late StreamSubscription> _groupMemberSub; late StreamSubscription> _streamKeyVerifications; @override void initState() { - _contactSub = twonlyDB.contactsDao - .watchContactAndVerificationState(widget.userId) - .listen(( - update, - ) { - if (update != null) { - setState(() { - _contact = update.$1; - }); - } + _contactSub = twonlyDB.contactsDao.watchContact(widget.userId).listen(( + update, + ) { + if (update != null) { + setState(() { + _contact = update; }); + } + }); _groupMemberSub = twonlyDB.groupsDao .watchContactGroupMember(widget.userId) .listen((groups) async { @@ -250,7 +248,7 @@ class _ContactViewState extends State { backgroundColor: context.color.surfaceContainer, collapsedShape: const RoundedRectangleBorder(), leading: Padding( - padding: EdgeInsetsGeometry.only(left: 12, right: 12), + padding: const EdgeInsetsGeometry.only(left: 12, right: 12), child: VerificationBadgeComp( contact: contact, size: 20, diff --git a/lib/src/visual/views/home.view.dart b/lib/src/visual/views/home.view.dart index d569f4d9..73a6fbf1 100644 --- a/lib/src/visual/views/home.view.dart +++ b/lib/src/visual/views/home.view.dart @@ -19,8 +19,6 @@ import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/visual/views/chats/chat_list.view.dart'; import 'package:twonly/src/visual/views/memories/memories.view.dart'; -void Function(int) globalUpdateOfHomeViewPageIndex = (a) {}; - class HomeView extends StatefulWidget { const HomeView({ required this.initialPage, @@ -32,68 +30,19 @@ class HomeView extends StatefulWidget { State createState() => HomeViewState(); } -class Shade extends StatelessWidget { - const Shade({required this.opacity, super.key}); - final double opacity; - - @override - Widget build(BuildContext context) { - return Positioned.fill( - child: Opacity( - opacity: opacity, - child: Container( - color: context.color.surface, - ), - ), - ); - } -} - class HomeViewState extends State { int _activePageIdx = 1; + double _offsetRatio = 0; + double _offsetFromOne = 0; + Timer? _disableCameraTimer; final MainCameraController _mainCameraController = MainCameraController(); - final PageController _homeViewPageController = PageController(initialPage: 1); + late StreamSubscription> _intentStreamSub; late StreamSubscription _deepLinkSub; - double buttonDiameter = 100; - double offsetRatio = 0; - double offsetFromOne = 0; - double lastChange = 0; - - Timer? disableCameraTimer; - - bool onPageView(ScrollNotification notification) { - disableCameraTimer?.cancel(); - if (notification.depth == 0 && notification is ScrollUpdateNotification) { - final page = _homeViewPageController.page ?? 0; - lastChange = page; - setState(() { - offsetFromOne = 1.0 - (_homeViewPageController.page ?? 0); - offsetRatio = offsetFromOne.abs(); - }); - } - if (_mainCameraController.cameraController == null && - !_mainCameraController.initCameraStarted && - offsetRatio < 1) { - unawaited( - _mainCameraController.selectCamera( - _mainCameraController.selectedCameraDetails.cameraId, - false, - ), - ); - } - if (offsetRatio == 1) { - disableCameraTimer = Timer(const Duration(milliseconds: 500), () async { - await _mainCameraController.closeCamera(); - _mainCameraController.sharedLinkForPreview = null; - disableCameraTimer = null; - }); - } - return false; - } + static final streamHomeViewPageIndex = StreamController.broadcast(); @override void initState() { @@ -102,31 +51,32 @@ class HomeViewState extends State { if (mounted) setState(() {}); }; - globalUpdateOfHomeViewPageIndex = (index) { + streamHomeViewPageIndex.stream.listen((index) { _homeViewPageController.jumpToPage(index); setState(() { _activePageIdx = index; }); - }; + }); + selectNotificationStream.stream.listen((response) async { if (response.payload != null && response.payload!.startsWith(Routes.chats) && response.payload! != Routes.chats) { await routerProvider.push(response.payload!); } - globalUpdateOfHomeViewPageIndex(0); + streamHomeViewPageIndex.add(0); }); + unawaited(_mainCameraController.selectCamera(0, true)); - unawaited(initAsync()); + unawaited(_initAsync()); // Subscribe to all events (initial link and further) _deepLinkSub = AppLinks().uriLinkStream.listen((uri) async { - if (mounted) { - Log.info('Got link via app links: ${uri.scheme}'); - if (!await handleIntentUrl(context, uri)) { - if (uri.scheme.startsWith('http')) { - _mainCameraController.setSharedLinkForPreview(uri); - } + if (!mounted) return; + Log.info('Got link via app links: ${uri.scheme}'); + if (!await handleIntentUrl(context, uri)) { + if (uri.scheme.startsWith('http')) { + _mainCameraController.setSharedLinkForPreview(uri); } } }); @@ -135,26 +85,17 @@ class HomeViewState extends State { context, _mainCameraController.setSharedLinkForPreview, ); + WidgetsBinding.instance.addPostFrameCallback((_) { if (widget.initialPage == 1 && !userService.currentUser.startWithCameraOpen || widget.initialPage == 0) { - globalUpdateOfHomeViewPageIndex(0); + streamHomeViewPageIndex.add(0); } }); } - @override - void dispose() { - unawaited(selectNotificationStream.close()); - disableCameraTimer?.cancel(); - _mainCameraController.closeCamera(); - _intentStreamSub.cancel(); - _deepLinkSub.cancel(); - super.dispose(); - } - - Future initAsync() async { + Future _initAsync() async { final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin .getNotificationAppLaunchDetails(); @@ -168,10 +109,10 @@ class HomeViewState extends State { payload.startsWith(Routes.chats) && payload != Routes.chats) { await routerProvider.push(payload); - globalUpdateOfHomeViewPageIndex(0); + streamHomeViewPageIndex.add(0); } if (payload == Routes.chats) { - globalUpdateOfHomeViewPageIndex(0); + streamHomeViewPageIndex.add(0); } } } @@ -191,22 +132,69 @@ class HomeViewState extends State { } } + @override + void dispose() { + selectNotificationStream.close(); + streamHomeViewPageIndex.close(); + _disableCameraTimer?.cancel(); + _mainCameraController.closeCamera(); + _intentStreamSub.cancel(); + _deepLinkSub.cancel(); + super.dispose(); + } + + bool _onPageView(ScrollNotification notification) { + _disableCameraTimer?.cancel(); + + if (notification.depth == 0 && notification is ScrollUpdateNotification) { + setState(() { + _offsetFromOne = 1.0 - (_homeViewPageController.page ?? 0); + _offsetRatio = _offsetFromOne.abs(); + }); + } + + if (_mainCameraController.cameraController == null && + !_mainCameraController.initCameraStarted && + _offsetRatio < 1) { + unawaited( + _mainCameraController.selectCamera( + _mainCameraController.selectedCameraDetails.cameraId, + false, + ), + ); + } + + if (_offsetRatio == 1) { + _disableCameraTimer = Timer(const Duration(milliseconds: 500), () async { + await _mainCameraController.closeCamera(); + _mainCameraController.sharedLinkForPreview = null; + _disableCameraTimer = null; + }); + } + return false; + } + @override Widget build(BuildContext context) { return Scaffold( body: GestureDetector( - onDoubleTap: offsetRatio == 0 + onDoubleTap: _offsetRatio == 0 ? _mainCameraController.onDoubleTap : null, - onTapDown: offsetRatio == 0 ? _mainCameraController.onTapDown : null, + onTapDown: _offsetRatio == 0 ? _mainCameraController.onTapDown : null, child: Stack( children: [ MainCameraPreview(mainCameraController: _mainCameraController), - Shade( - opacity: offsetRatio, + Positioned.fill( + child: Opacity( + opacity: _offsetRatio, + child: Container( + color: context.color.surface, + ), + ), ), NotificationListener( - onNotification: onPageView, + onNotification: _onPageView, child: Positioned.fill( child: PageView( controller: _homeViewPageController, @@ -227,15 +215,16 @@ class HomeViewState extends State { left: 0, top: 0, right: 0, - bottom: (offsetRatio > 0.25) + bottom: (_offsetRatio > 0.25) ? MediaQuery.sizeOf(context).height * 2 : 0, child: Opacity( - opacity: 1 - (offsetRatio * 4) % 1, + opacity: 1 - (_offsetRatio * 4) % 1, child: CameraPreviewControllerView( mainController: _mainCameraController, isVisible: - ((1 - (offsetRatio * 4) % 1) == 1) && _activePageIdx == 1, + ((1 - (_offsetRatio * 4) % 1) == 1) && + _activePageIdx == 1, ), ), ), diff --git a/lib/src/visual/views/public_profile.view.dart b/lib/src/visual/views/public_profile.view.dart index fd3b97c6..4c328fed 100644 --- a/lib/src/visual/views/public_profile.view.dart +++ b/lib/src/visual/views/public_profile.view.dart @@ -11,7 +11,7 @@ import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/utils/qr.dart'; +import 'package:twonly/src/utils/qr.utils.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; class PublicProfileView extends StatefulWidget {