handle scanned qr link via intent

This commit is contained in:
otsmr 2026-04-22 14:06:05 +02:00
parent 7d09bd7283
commit 95c5d6a4f1
11 changed files with 245 additions and 162 deletions

View file

@ -113,7 +113,6 @@ class AppMainWidget extends StatefulWidget {
class _AppMainWidgetState extends State<AppMainWidget> {
bool _isUserCreated = false;
bool _showDatabaseMigration = false;
bool _showOnboarding = true;
bool _isLoaded = false;
bool _skipBackup = false;
@ -135,18 +134,12 @@ class _AppMainWidgetState extends State<AppMainWidget> {
// 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<AppMainWidget> {
late Widget child;
if (_showDatabaseMigration) {
child = const Center(child: Text('Please reinstall twonly.'));
} else if (_isUserCreated) {
if (_isUserCreated) {
if (_isTwonlyLocked) {
child = UnlockTwonlyView(
callbackOnSuccess: () => setState(() {

View file

@ -99,21 +99,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> 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<List<Contact>> getAllContacts() {
return select(contacts).get();
}

View file

@ -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<VerificationType>()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {contactId};
}
@DataClassName('VerificationToken')

View file

@ -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<int> verificationId = GeneratedColumn<int>(
'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<GeneratedColumn> get $columns => [contactId, type, createdAt];
List<GeneratedColumn> 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<GeneratedColumn> get $primaryKey => {contactId};
Set<GeneratedColumn> get $primaryKey => {verificationId};
@override
KeyVerification map(Map<String, dynamic> 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<KeyVerification> {
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<KeyVerification> {
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['verification_id'] = Variable<int>(verificationId);
map['contact_id'] = Variable<int>(contactId);
{
map['type'] = Variable<String>(
@ -9161,6 +9199,7 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
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<KeyVerification> {
}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return KeyVerification(
verificationId: serializer.fromJson<int>(json['verificationId']),
contactId: serializer.fromJson<int>(json['contactId']),
type: $KeyVerificationsTable.$convertertype.fromJson(
serializer.fromJson<String>(json['type']),
@ -9184,6 +9224,7 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'verificationId': serializer.toJson<int>(verificationId),
'contactId': serializer.toJson<int>(contactId),
'type': serializer.toJson<String>(
$KeyVerificationsTable.$convertertype.toJson(type),
@ -9193,16 +9234,21 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
}
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<KeyVerification> {
@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<KeyVerification> {
}
@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<KeyVerification> {
final Value<int> verificationId;
final Value<int> contactId;
final Value<VerificationType> type;
final Value<DateTime> 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<KeyVerification> custom({
Expression<int>? verificationId,
Expression<int>? contactId,
Expression<String>? type,
Expression<DateTime>? 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<KeyVerification> {
}
KeyVerificationsCompanion copyWith({
Value<int>? verificationId,
Value<int>? contactId,
Value<VerificationType>? type,
Value<DateTime>? 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<KeyVerification> {
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (verificationId.present) {
map['verification_id'] = Variable<int>(verificationId.value);
}
if (contactId.present) {
map['contact_id'] = Variable<int>(contactId.value);
}
@ -9288,6 +9347,7 @@ class KeyVerificationsCompanion extends UpdateCompanion<KeyVerification> {
@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<int> contactId,
Value<int> verificationId,
required int contactId,
required VerificationType type,
Value<DateTime> createdAt,
});
typedef $$KeyVerificationsTableUpdateCompanionBuilder =
KeyVerificationsCompanion Function({
Value<int> verificationId,
Value<int> contactId,
Value<VerificationType> type,
Value<DateTime> createdAt,
@ -19522,6 +19584,11 @@ class $$KeyVerificationsTableFilterComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<int> get verificationId => $composableBuilder(
column: $table.verificationId,
builder: (column) => ColumnFilters(column),
);
ColumnWithTypeConverterFilters<VerificationType, VerificationType, String>
get type => $composableBuilder(
column: $table.type,
@ -19566,6 +19633,11 @@ class $$KeyVerificationsTableOrderingComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<int> get verificationId => $composableBuilder(
column: $table.verificationId,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get type => $composableBuilder(
column: $table.type,
builder: (column) => ColumnOrderings(column),
@ -19609,6 +19681,11 @@ class $$KeyVerificationsTableAnnotationComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<int> get verificationId => $composableBuilder(
column: $table.verificationId,
builder: (column) => column,
);
GeneratedColumnWithTypeConverter<VerificationType, String> get type =>
$composableBuilder(column: $table.type, builder: (column) => column);
@ -19669,20 +19746,24 @@ class $$KeyVerificationsTableTableManager
$$KeyVerificationsTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
Value<int> verificationId = const Value.absent(),
Value<int> contactId = const Value.absent(),
Value<VerificationType> type = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
}) => KeyVerificationsCompanion(
verificationId: verificationId,
contactId: contactId,
type: type,
createdAt: createdAt,
),
createCompanionCallback:
({
Value<int> contactId = const Value.absent(),
Value<int> verificationId = const Value.absent(),
required int contactId,
required VerificationType type,
Value<DateTime> createdAt = const Value.absent(),
}) => KeyVerificationsCompanion.insert(
verificationId: verificationId,
contactId: contactId,
type: type,
createdAt: createdAt,

View file

@ -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<bool> 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<bool> 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 (contacts.isEmpty) {
if (!context.mounted) return true;
if (contacts.isEmpty) {
// 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(
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<bool> 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<void> _pubKeysDoNotMatch(BuildContext context, String username) async {
await showAlertDialog(
context,
context.lang.couldNotVerifyUsername(username),
context.lang.linkPubkeyDoesNotMatch,
customCancel: '',
);
}
Future<void> handleIntentMediaFile(
BuildContext context,
String filePath,

View file

@ -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<CameraPreviewView> {
// 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);
}

View file

@ -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<int, ScannedVerifiedContact> contactsVerified = {};
Map<int, ScannedNewProfile> scannedNewProfiles = {};
Set<String> _handledProfileLinks = {};
final Set<String> _handledProfileLinks = {};
String? scannedUrl;
GlobalKey zoomButtonKey = GlobalKey();
GlobalKey cameraPreviewKey = GlobalKey();
@ -381,6 +381,7 @@ class MainCameraController {
);
}
}
continue;
}
if (link.startsWith('http://') || link.startsWith('https://')) {

View file

@ -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,20 +35,18 @@ class _ContactViewState extends State<ContactView> {
List<GroupMember> _memberOfGroups = [];
List<KeyVerification> _keyVerifications = [];
late StreamSubscription<(Contact, bool)?> _contactSub;
late StreamSubscription<Contact?> _contactSub;
late StreamSubscription<List<GroupMember>> _groupMemberSub;
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
@override
void initState() {
_contactSub = twonlyDB.contactsDao
.watchContactAndVerificationState(widget.userId)
.listen((
_contactSub = twonlyDB.contactsDao.watchContact(widget.userId).listen((
update,
) {
if (update != null) {
setState(() {
_contact = update.$1;
_contact = update;
});
}
});
@ -250,7 +248,7 @@ class _ContactViewState extends State<ContactView> {
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,

View file

@ -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<HomeView> 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<HomeView> {
int _activePageIdx = 1;
double _offsetRatio = 0;
double _offsetFromOne = 0;
Timer? _disableCameraTimer;
final MainCameraController _mainCameraController = MainCameraController();
final PageController _homeViewPageController = PageController(initialPage: 1);
late StreamSubscription<List<SharedFile>> _intentStreamSub;
late StreamSubscription<Uri> _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<int>.broadcast();
@override
void initState() {
@ -102,59 +51,51 @@ class HomeViewState extends State<HomeView> {
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) {
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);
}
}
}
});
_intentStreamSub = initIntentStreams(
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<void> initAsync() async {
Future<void> _initAsync() async {
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
.getNotificationAppLaunchDetails();
@ -168,10 +109,10 @@ class HomeViewState extends State<HomeView> {
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<HomeView> {
}
}
@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: <Widget>[
MainCameraPreview(mainCameraController: _mainCameraController),
Shade(
opacity: offsetRatio,
Positioned.fill(
child: Opacity(
opacity: _offsetRatio,
child: Container(
color: context.color.surface,
),
),
),
NotificationListener<ScrollNotification>(
onNotification: onPageView,
onNotification: _onPageView,
child: Positioned.fill(
child: PageView(
controller: _homeViewPageController,
@ -227,15 +215,16 @@ class HomeViewState extends State<HomeView> {
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,
),
),
),

View file

@ -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 {