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> { class _AppMainWidgetState extends State<AppMainWidget> {
bool _isUserCreated = false; bool _isUserCreated = false;
bool _showDatabaseMigration = false;
bool _showOnboarding = true; bool _showOnboarding = true;
bool _isLoaded = false; bool _isLoaded = false;
bool _skipBackup = 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 // do not change in case twonly was already unlocked at some point
_isTwonlyLocked = userService.currentUser.screenLockEnabled; _isTwonlyLocked = userService.currentUser.screenLockEnabled;
} }
if (userService.currentUser.appVersion < 62) { } else {
_showDatabaseMigration = true;
}
}
if (!_isUserCreated && !_showDatabaseMigration) {
// This means the user is in the onboarding screen, so start with the Proof of Work. // This means the user is in the onboarding screen, so start with the Proof of Work.
final (proof, disabled) = await apiService.getProofOfWork(); final (proof, disabled) = await apiService.getProofOfWork();
if (proof != null) { if (proof != null) {
Log.info('Starting with proof of work calculation.'); Log.info('Starting with proof of work calculation.');
// Starting with the proof of work.
_proofOfWork = ( _proofOfWork = (
calculatePoW(proof.prefix, proof.difficulty.toInt()), calculatePoW(proof.prefix, proof.difficulty.toInt()),
false, false,
@ -169,9 +162,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
late Widget child; late Widget child;
if (_showDatabaseMigration) { if (_isUserCreated) {
child = const Center(child: Text('Please reinstall twonly.'));
} else if (_isUserCreated) {
if (_isTwonlyLocked) { if (_isTwonlyLocked) {
child = UnlockTwonlyView( child = UnlockTwonlyView(
callbackOnSuccess: () => setState(() { callbackOnSuccess: () => setState(() {

View file

@ -99,21 +99,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
)..where((t) => t.userId.equals(userid))).watchSingleOrNull(); )..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() { Future<List<Contact>> getAllContacts() {
return select(contacts).get(); return select(contacts).get();
} }

View file

@ -47,18 +47,14 @@ enum VerificationType {
@DataClassName('KeyVerification') @DataClassName('KeyVerification')
class KeyVerifications extends Table { class KeyVerifications extends Table {
IntColumn get verificationId => integer().autoIncrement()();
IntColumn get contactId => integer().references( IntColumn get contactId => integer().references(
Contacts, Contacts,
#userId, #userId,
onDelete: KeyAction.cascade, onDelete: KeyAction.cascade,
)(); )();
TextColumn get type => textEnum<VerificationType>()(); TextColumn get type => textEnum<VerificationType>()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {contactId};
} }
@DataClassName('VerificationToken') @DataClassName('VerificationToken')

View file

@ -9041,6 +9041,21 @@ class $KeyVerificationsTable extends KeyVerifications
final GeneratedDatabase attachedDatabase; final GeneratedDatabase attachedDatabase;
final String? _alias; final String? _alias;
$KeyVerificationsTable(this.attachedDatabase, [this._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( static const VerificationMeta _contactIdMeta = const VerificationMeta(
'contactId', 'contactId',
); );
@ -9050,7 +9065,7 @@ class $KeyVerificationsTable extends KeyVerifications
aliasedName, aliasedName,
false, false,
type: DriftSqlType.int, type: DriftSqlType.int,
requiredDuringInsert: false, requiredDuringInsert: true,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'REFERENCES contacts (user_id) ON DELETE CASCADE', 'REFERENCES contacts (user_id) ON DELETE CASCADE',
), ),
@ -9077,7 +9092,12 @@ class $KeyVerificationsTable extends KeyVerifications
defaultValue: currentDateAndTime, defaultValue: currentDateAndTime,
); );
@override @override
List<GeneratedColumn> get $columns => [contactId, type, createdAt]; List<GeneratedColumn> get $columns => [
verificationId,
contactId,
type,
createdAt,
];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
@ -9090,11 +9110,22 @@ class $KeyVerificationsTable extends KeyVerifications
}) { }) {
final context = VerificationContext(); final context = VerificationContext();
final data = instance.toColumns(true); final data = instance.toColumns(true);
if (data.containsKey('verification_id')) {
context.handle(
_verificationIdMeta,
verificationId.isAcceptableOrUnknown(
data['verification_id']!,
_verificationIdMeta,
),
);
}
if (data.containsKey('contact_id')) { if (data.containsKey('contact_id')) {
context.handle( context.handle(
_contactIdMeta, _contactIdMeta,
contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta), contactId.isAcceptableOrUnknown(data['contact_id']!, _contactIdMeta),
); );
} else if (isInserting) {
context.missing(_contactIdMeta);
} }
if (data.containsKey('created_at')) { if (data.containsKey('created_at')) {
context.handle( context.handle(
@ -9106,11 +9137,15 @@ class $KeyVerificationsTable extends KeyVerifications
} }
@override @override
Set<GeneratedColumn> get $primaryKey => {contactId}; Set<GeneratedColumn> get $primaryKey => {verificationId};
@override @override
KeyVerification map(Map<String, dynamic> data, {String? tablePrefix}) { KeyVerification map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return KeyVerification( return KeyVerification(
verificationId: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}verification_id'],
)!,
contactId: attachedDatabase.typeMapping.read( contactId: attachedDatabase.typeMapping.read(
DriftSqlType.int, DriftSqlType.int,
data['${effectivePrefix}contact_id'], data['${effectivePrefix}contact_id'],
@ -9138,10 +9173,12 @@ class $KeyVerificationsTable extends KeyVerifications
} }
class KeyVerification extends DataClass implements Insertable<KeyVerification> { class KeyVerification extends DataClass implements Insertable<KeyVerification> {
final int verificationId;
final int contactId; final int contactId;
final VerificationType type; final VerificationType type;
final DateTime createdAt; final DateTime createdAt;
const KeyVerification({ const KeyVerification({
required this.verificationId,
required this.contactId, required this.contactId,
required this.type, required this.type,
required this.createdAt, required this.createdAt,
@ -9149,6 +9186,7 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
map['verification_id'] = Variable<int>(verificationId);
map['contact_id'] = Variable<int>(contactId); map['contact_id'] = Variable<int>(contactId);
{ {
map['type'] = Variable<String>( map['type'] = Variable<String>(
@ -9161,6 +9199,7 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
KeyVerificationsCompanion toCompanion(bool nullToAbsent) { KeyVerificationsCompanion toCompanion(bool nullToAbsent) {
return KeyVerificationsCompanion( return KeyVerificationsCompanion(
verificationId: Value(verificationId),
contactId: Value(contactId), contactId: Value(contactId),
type: Value(type), type: Value(type),
createdAt: Value(createdAt), createdAt: Value(createdAt),
@ -9173,6 +9212,7 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
}) { }) {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return KeyVerification( return KeyVerification(
verificationId: serializer.fromJson<int>(json['verificationId']),
contactId: serializer.fromJson<int>(json['contactId']), contactId: serializer.fromJson<int>(json['contactId']),
type: $KeyVerificationsTable.$convertertype.fromJson( type: $KeyVerificationsTable.$convertertype.fromJson(
serializer.fromJson<String>(json['type']), serializer.fromJson<String>(json['type']),
@ -9184,6 +9224,7 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
Map<String, dynamic> toJson({ValueSerializer? serializer}) { Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{ return <String, dynamic>{
'verificationId': serializer.toJson<int>(verificationId),
'contactId': serializer.toJson<int>(contactId), 'contactId': serializer.toJson<int>(contactId),
'type': serializer.toJson<String>( 'type': serializer.toJson<String>(
$KeyVerificationsTable.$convertertype.toJson(type), $KeyVerificationsTable.$convertertype.toJson(type),
@ -9193,16 +9234,21 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
} }
KeyVerification copyWith({ KeyVerification copyWith({
int? verificationId,
int? contactId, int? contactId,
VerificationType? type, VerificationType? type,
DateTime? createdAt, DateTime? createdAt,
}) => KeyVerification( }) => KeyVerification(
verificationId: verificationId ?? this.verificationId,
contactId: contactId ?? this.contactId, contactId: contactId ?? this.contactId,
type: type ?? this.type, type: type ?? this.type,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
); );
KeyVerification copyWithCompanion(KeyVerificationsCompanion data) { KeyVerification copyWithCompanion(KeyVerificationsCompanion data) {
return KeyVerification( return KeyVerification(
verificationId: data.verificationId.present
? data.verificationId.value
: this.verificationId,
contactId: data.contactId.present ? data.contactId.value : this.contactId, contactId: data.contactId.present ? data.contactId.value : this.contactId,
type: data.type.present ? data.type.value : this.type, type: data.type.present ? data.type.value : this.type,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
@ -9212,6 +9258,7 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
@override @override
String toString() { String toString() {
return (StringBuffer('KeyVerification(') return (StringBuffer('KeyVerification(')
..write('verificationId: $verificationId, ')
..write('contactId: $contactId, ') ..write('contactId: $contactId, ')
..write('type: $type, ') ..write('type: $type, ')
..write('createdAt: $createdAt') ..write('createdAt: $createdAt')
@ -9220,36 +9267,43 @@ class KeyVerification extends DataClass implements Insertable<KeyVerification> {
} }
@override @override
int get hashCode => Object.hash(contactId, type, createdAt); int get hashCode => Object.hash(verificationId, contactId, type, createdAt);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
(other is KeyVerification && (other is KeyVerification &&
other.verificationId == this.verificationId &&
other.contactId == this.contactId && other.contactId == this.contactId &&
other.type == this.type && other.type == this.type &&
other.createdAt == this.createdAt); other.createdAt == this.createdAt);
} }
class KeyVerificationsCompanion extends UpdateCompanion<KeyVerification> { class KeyVerificationsCompanion extends UpdateCompanion<KeyVerification> {
final Value<int> verificationId;
final Value<int> contactId; final Value<int> contactId;
final Value<VerificationType> type; final Value<VerificationType> type;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
const KeyVerificationsCompanion({ const KeyVerificationsCompanion({
this.verificationId = const Value.absent(),
this.contactId = const Value.absent(), this.contactId = const Value.absent(),
this.type = const Value.absent(), this.type = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
}); });
KeyVerificationsCompanion.insert({ KeyVerificationsCompanion.insert({
this.contactId = const Value.absent(), this.verificationId = const Value.absent(),
required int contactId,
required VerificationType type, required VerificationType type,
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
}) : type = Value(type); }) : contactId = Value(contactId),
type = Value(type);
static Insertable<KeyVerification> custom({ static Insertable<KeyVerification> custom({
Expression<int>? verificationId,
Expression<int>? contactId, Expression<int>? contactId,
Expression<String>? type, Expression<String>? type,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
if (verificationId != null) 'verification_id': verificationId,
if (contactId != null) 'contact_id': contactId, if (contactId != null) 'contact_id': contactId,
if (type != null) 'type': type, if (type != null) 'type': type,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
@ -9257,11 +9311,13 @@ class KeyVerificationsCompanion extends UpdateCompanion<KeyVerification> {
} }
KeyVerificationsCompanion copyWith({ KeyVerificationsCompanion copyWith({
Value<int>? verificationId,
Value<int>? contactId, Value<int>? contactId,
Value<VerificationType>? type, Value<VerificationType>? type,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
}) { }) {
return KeyVerificationsCompanion( return KeyVerificationsCompanion(
verificationId: verificationId ?? this.verificationId,
contactId: contactId ?? this.contactId, contactId: contactId ?? this.contactId,
type: type ?? this.type, type: type ?? this.type,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
@ -9271,6 +9327,9 @@ class KeyVerificationsCompanion extends UpdateCompanion<KeyVerification> {
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
if (verificationId.present) {
map['verification_id'] = Variable<int>(verificationId.value);
}
if (contactId.present) { if (contactId.present) {
map['contact_id'] = Variable<int>(contactId.value); map['contact_id'] = Variable<int>(contactId.value);
} }
@ -9288,6 +9347,7 @@ class KeyVerificationsCompanion extends UpdateCompanion<KeyVerification> {
@override @override
String toString() { String toString() {
return (StringBuffer('KeyVerificationsCompanion(') return (StringBuffer('KeyVerificationsCompanion(')
..write('verificationId: $verificationId, ')
..write('contactId: $contactId, ') ..write('contactId: $contactId, ')
..write('type: $type, ') ..write('type: $type, ')
..write('createdAt: $createdAt') ..write('createdAt: $createdAt')
@ -19473,12 +19533,14 @@ typedef $$GroupHistoriesTableProcessedTableManager =
>; >;
typedef $$KeyVerificationsTableCreateCompanionBuilder = typedef $$KeyVerificationsTableCreateCompanionBuilder =
KeyVerificationsCompanion Function({ KeyVerificationsCompanion Function({
Value<int> contactId, Value<int> verificationId,
required int contactId,
required VerificationType type, required VerificationType type,
Value<DateTime> createdAt, Value<DateTime> createdAt,
}); });
typedef $$KeyVerificationsTableUpdateCompanionBuilder = typedef $$KeyVerificationsTableUpdateCompanionBuilder =
KeyVerificationsCompanion Function({ KeyVerificationsCompanion Function({
Value<int> verificationId,
Value<int> contactId, Value<int> contactId,
Value<VerificationType> type, Value<VerificationType> type,
Value<DateTime> createdAt, Value<DateTime> createdAt,
@ -19522,6 +19584,11 @@ class $$KeyVerificationsTableFilterComposer
super.$addJoinBuilderToRootComposer, super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer, super.$removeJoinBuilderFromRootComposer,
}); });
ColumnFilters<int> get verificationId => $composableBuilder(
column: $table.verificationId,
builder: (column) => ColumnFilters(column),
);
ColumnWithTypeConverterFilters<VerificationType, VerificationType, String> ColumnWithTypeConverterFilters<VerificationType, VerificationType, String>
get type => $composableBuilder( get type => $composableBuilder(
column: $table.type, column: $table.type,
@ -19566,6 +19633,11 @@ class $$KeyVerificationsTableOrderingComposer
super.$addJoinBuilderToRootComposer, super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer, super.$removeJoinBuilderFromRootComposer,
}); });
ColumnOrderings<int> get verificationId => $composableBuilder(
column: $table.verificationId,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get type => $composableBuilder( ColumnOrderings<String> get type => $composableBuilder(
column: $table.type, column: $table.type,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@ -19609,6 +19681,11 @@ class $$KeyVerificationsTableAnnotationComposer
super.$addJoinBuilderToRootComposer, super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer, super.$removeJoinBuilderFromRootComposer,
}); });
GeneratedColumn<int> get verificationId => $composableBuilder(
column: $table.verificationId,
builder: (column) => column,
);
GeneratedColumnWithTypeConverter<VerificationType, String> get type => GeneratedColumnWithTypeConverter<VerificationType, String> get type =>
$composableBuilder(column: $table.type, builder: (column) => column); $composableBuilder(column: $table.type, builder: (column) => column);
@ -19669,20 +19746,24 @@ class $$KeyVerificationsTableTableManager
$$KeyVerificationsTableAnnotationComposer($db: db, $table: table), $$KeyVerificationsTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: updateCompanionCallback:
({ ({
Value<int> verificationId = const Value.absent(),
Value<int> contactId = const Value.absent(), Value<int> contactId = const Value.absent(),
Value<VerificationType> type = const Value.absent(), Value<VerificationType> type = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
}) => KeyVerificationsCompanion( }) => KeyVerificationsCompanion(
verificationId: verificationId,
contactId: contactId, contactId: contactId,
type: type, type: type,
createdAt: createdAt, createdAt: createdAt,
), ),
createCompanionCallback: createCompanionCallback:
({ ({
Value<int> contactId = const Value.absent(), Value<int> verificationId = const Value.absent(),
required int contactId,
required VerificationType type, required VerificationType type,
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
}) => KeyVerificationsCompanion.insert( }) => KeyVerificationsCompanion.insert(
verificationId: verificationId,
contactId: contactId, contactId: contactId,
type: type, type: type,
createdAt: createdAt, 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/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.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/components/alert.dialog.dart';
import 'package:twonly/src/visual/views/camera/share_image_editor.view.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'; 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.host != 'me.twonly.eu') return false;
if (uri.hasEmptyPath) 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 publicKey = uri.hasFragment ? uri.fragment : null;
final userPaths = uri.path.split('/'); final userPaths = uri.path.split('/');
if (userPaths.length != 2) return false; if (userPaths.length != 2) return false;
@ -40,25 +76,26 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
Log.info( Log.info(
'Opened via deep link!: username = $username public_key = ${uri.fragment}', 'Opened via deep link!: username = $username public_key = ${uri.fragment}',
); );
final contacts = await twonlyDB.contactsDao.getContactsByUsername(username); final contacts = await twonlyDB.contactsDao.getContactsByUsername(username);
if (contacts.isEmpty) {
if (!context.mounted) return true; if (!context.mounted) return true;
if (contacts.isEmpty) {
// User does not yet exists, making a request...
Uint8List? publicKeyBytes; Uint8List? publicKeyBytes;
if (publicKey != null) { if (publicKey != null) {
publicKeyBytes = base64Url.decode(publicKey); publicKeyBytes = base64Url.decode(publicKey);
} }
await Navigator.push( await context.navPush(
context, AddNewUserView(
MaterialPageRoute(
builder: (context) {
return AddNewUserView(
username: username, username: username,
publicKey: publicKeyBytes, publicKey: publicKeyBytes,
);
},
), ),
); );
} else if (publicKey != null) { return true;
}
if (publicKey != null) {
try { try {
final contact = contacts.first; final contact = contacts.first;
final storedPublicKey = await getPublicKeyFromContact(contact.userId); 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)); await context.push(Routes.profileContact(contact.userId));
} }
} else { } else {
await showAlertDialog( await _pubKeysDoNotMatch(context, contact.username);
context,
context.lang.couldNotVerifyUsername(contact.username),
context.lang.linkPubkeyDoesNotMatch,
customCancel: '',
);
} }
} catch (e) { } catch (e) {
Log.warn(e); Log.warn(e);
} }
} }
return true; return true;
} }
Future<void> _pubKeysDoNotMatch(BuildContext context, String username) async {
await showAlertDialog(
context,
context.lang.couldNotVerifyUsername(username),
context.lang.linkPubkeyDoesNotMatch,
customCancel: '',
);
}
Future<void> handleIntentMediaFile( Future<void> handleIntentMediaFile(
BuildContext context, BuildContext context,
String filePath, 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/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.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/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart'; import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart';
import 'package:twonly/src/visual/helpers/screenshot.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 // shouldReturn is null when the user used the back button
if (shouldReturn != null && shouldReturn) { if (shouldReturn != null && shouldReturn) {
if (widget.sendToGroup == null) { if (widget.sendToGroup == null) {
globalUpdateOfHomeViewPageIndex(0); HomeViewState.streamHomeViewPageIndex.add(0);
} else if (mounted) { } else if (mounted) {
Navigator.pop(context); 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/model/protobuf/client/generated/qr.pb.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.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/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/camera_preview_controller_view.dart';
import 'package:twonly/src/visual/views/camera/camera_preview_components/face_filters.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/face_filters.dart';
@ -45,7 +45,7 @@ class MainCameraController {
bool initCameraStarted = true; bool initCameraStarted = true;
Map<int, ScannedVerifiedContact> contactsVerified = {}; Map<int, ScannedVerifiedContact> contactsVerified = {};
Map<int, ScannedNewProfile> scannedNewProfiles = {}; Map<int, ScannedNewProfile> scannedNewProfiles = {};
Set<String> _handledProfileLinks = {}; final Set<String> _handledProfileLinks = {};
String? scannedUrl; String? scannedUrl;
GlobalKey zoomButtonKey = GlobalKey(); GlobalKey zoomButtonKey = GlobalKey();
GlobalKey cameraPreviewKey = GlobalKey(); GlobalKey cameraPreviewKey = GlobalKey();
@ -381,6 +381,7 @@ class MainCameraController {
); );
} }
} }
continue;
} }
if (link.startsWith('http://') || link.startsWith('https://')) { if (link.startsWith('http://') || link.startsWith('https://')) {

View file

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:intl/intl.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
@ -35,20 +35,18 @@ class _ContactViewState extends State<ContactView> {
List<GroupMember> _memberOfGroups = []; List<GroupMember> _memberOfGroups = [];
List<KeyVerification> _keyVerifications = []; List<KeyVerification> _keyVerifications = [];
late StreamSubscription<(Contact, bool)?> _contactSub; late StreamSubscription<Contact?> _contactSub;
late StreamSubscription<List<GroupMember>> _groupMemberSub; late StreamSubscription<List<GroupMember>> _groupMemberSub;
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications; late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
@override @override
void initState() { void initState() {
_contactSub = twonlyDB.contactsDao _contactSub = twonlyDB.contactsDao.watchContact(widget.userId).listen((
.watchContactAndVerificationState(widget.userId)
.listen((
update, update,
) { ) {
if (update != null) { if (update != null) {
setState(() { setState(() {
_contact = update.$1; _contact = update;
}); });
} }
}); });
@ -250,7 +248,7 @@ class _ContactViewState extends State<ContactView> {
backgroundColor: context.color.surfaceContainer, backgroundColor: context.color.surfaceContainer,
collapsedShape: const RoundedRectangleBorder(), collapsedShape: const RoundedRectangleBorder(),
leading: Padding( leading: Padding(
padding: EdgeInsetsGeometry.only(left: 12, right: 12), padding: const EdgeInsetsGeometry.only(left: 12, right: 12),
child: VerificationBadgeComp( child: VerificationBadgeComp(
contact: contact, contact: contact,
size: 20, 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/chats/chat_list.view.dart';
import 'package:twonly/src/visual/views/memories/memories.view.dart'; import 'package:twonly/src/visual/views/memories/memories.view.dart';
void Function(int) globalUpdateOfHomeViewPageIndex = (a) {};
class HomeView extends StatefulWidget { class HomeView extends StatefulWidget {
const HomeView({ const HomeView({
required this.initialPage, required this.initialPage,
@ -32,68 +30,19 @@ class HomeView extends StatefulWidget {
State<HomeView> createState() => HomeViewState(); 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> { class HomeViewState extends State<HomeView> {
int _activePageIdx = 1; int _activePageIdx = 1;
double _offsetRatio = 0;
double _offsetFromOne = 0;
Timer? _disableCameraTimer;
final MainCameraController _mainCameraController = MainCameraController(); final MainCameraController _mainCameraController = MainCameraController();
final PageController _homeViewPageController = PageController(initialPage: 1); final PageController _homeViewPageController = PageController(initialPage: 1);
late StreamSubscription<List<SharedFile>> _intentStreamSub; late StreamSubscription<List<SharedFile>> _intentStreamSub;
late StreamSubscription<Uri> _deepLinkSub; late StreamSubscription<Uri> _deepLinkSub;
double buttonDiameter = 100; static final streamHomeViewPageIndex = StreamController<int>.broadcast();
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;
}
@override @override
void initState() { void initState() {
@ -102,59 +51,51 @@ class HomeViewState extends State<HomeView> {
if (mounted) setState(() {}); if (mounted) setState(() {});
}; };
globalUpdateOfHomeViewPageIndex = (index) { streamHomeViewPageIndex.stream.listen((index) {
_homeViewPageController.jumpToPage(index); _homeViewPageController.jumpToPage(index);
setState(() { setState(() {
_activePageIdx = index; _activePageIdx = index;
}); });
}; });
selectNotificationStream.stream.listen((response) async { selectNotificationStream.stream.listen((response) async {
if (response.payload != null && if (response.payload != null &&
response.payload!.startsWith(Routes.chats) && response.payload!.startsWith(Routes.chats) &&
response.payload! != Routes.chats) { response.payload! != Routes.chats) {
await routerProvider.push(response.payload!); await routerProvider.push(response.payload!);
} }
globalUpdateOfHomeViewPageIndex(0); streamHomeViewPageIndex.add(0);
}); });
unawaited(_mainCameraController.selectCamera(0, true)); unawaited(_mainCameraController.selectCamera(0, true));
unawaited(initAsync()); unawaited(_initAsync());
// Subscribe to all events (initial link and further) // Subscribe to all events (initial link and further)
_deepLinkSub = AppLinks().uriLinkStream.listen((uri) async { _deepLinkSub = AppLinks().uriLinkStream.listen((uri) async {
if (mounted) { if (!mounted) return;
Log.info('Got link via app links: ${uri.scheme}'); Log.info('Got link via app links: ${uri.scheme}');
if (!await handleIntentUrl(context, uri)) { if (!await handleIntentUrl(context, uri)) {
if (uri.scheme.startsWith('http')) { if (uri.scheme.startsWith('http')) {
_mainCameraController.setSharedLinkForPreview(uri); _mainCameraController.setSharedLinkForPreview(uri);
} }
} }
}
}); });
_intentStreamSub = initIntentStreams( _intentStreamSub = initIntentStreams(
context, context,
_mainCameraController.setSharedLinkForPreview, _mainCameraController.setSharedLinkForPreview,
); );
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.initialPage == 1 && if (widget.initialPage == 1 &&
!userService.currentUser.startWithCameraOpen || !userService.currentUser.startWithCameraOpen ||
widget.initialPage == 0) { widget.initialPage == 0) {
globalUpdateOfHomeViewPageIndex(0); streamHomeViewPageIndex.add(0);
} }
}); });
} }
@override Future<void> _initAsync() async {
void dispose() {
unawaited(selectNotificationStream.close());
disableCameraTimer?.cancel();
_mainCameraController.closeCamera();
_intentStreamSub.cancel();
_deepLinkSub.cancel();
super.dispose();
}
Future<void> initAsync() async {
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
.getNotificationAppLaunchDetails(); .getNotificationAppLaunchDetails();
@ -168,10 +109,10 @@ class HomeViewState extends State<HomeView> {
payload.startsWith(Routes.chats) && payload.startsWith(Routes.chats) &&
payload != Routes.chats) { payload != Routes.chats) {
await routerProvider.push(payload); await routerProvider.push(payload);
globalUpdateOfHomeViewPageIndex(0); streamHomeViewPageIndex.add(0);
} }
if (payload == Routes.chats) { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: GestureDetector( body: GestureDetector(
onDoubleTap: offsetRatio == 0 onDoubleTap: _offsetRatio == 0
? _mainCameraController.onDoubleTap ? _mainCameraController.onDoubleTap
: null, : null,
onTapDown: offsetRatio == 0 ? _mainCameraController.onTapDown : null, onTapDown: _offsetRatio == 0 ? _mainCameraController.onTapDown : null,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
MainCameraPreview(mainCameraController: _mainCameraController), MainCameraPreview(mainCameraController: _mainCameraController),
Shade( Positioned.fill(
opacity: offsetRatio, child: Opacity(
opacity: _offsetRatio,
child: Container(
color: context.color.surface,
),
),
), ),
NotificationListener<ScrollNotification>( NotificationListener<ScrollNotification>(
onNotification: onPageView, onNotification: _onPageView,
child: Positioned.fill( child: Positioned.fill(
child: PageView( child: PageView(
controller: _homeViewPageController, controller: _homeViewPageController,
@ -227,15 +215,16 @@ class HomeViewState extends State<HomeView> {
left: 0, left: 0,
top: 0, top: 0,
right: 0, right: 0,
bottom: (offsetRatio > 0.25) bottom: (_offsetRatio > 0.25)
? MediaQuery.sizeOf(context).height * 2 ? MediaQuery.sizeOf(context).height * 2
: 0, : 0,
child: Opacity( child: Opacity(
opacity: 1 - (offsetRatio * 4) % 1, opacity: 1 - (_offsetRatio * 4) % 1,
child: CameraPreviewControllerView( child: CameraPreviewControllerView(
mainController: _mainCameraController, mainController: _mainCameraController,
isVisible: 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/services/signal/identity.signal.dart';
import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/misc.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'; import 'package:twonly/src/visual/elements/better_list_title.element.dart';
class PublicProfileView extends StatefulWidget { class PublicProfileView extends StatefulWidget {