restructure code

This commit is contained in:
otsmr 2026-04-21 17:29:01 +02:00
parent ba2f9644c0
commit 1c902bb64d
252 changed files with 1579 additions and 3173 deletions

View file

@ -1,7 +1,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:twonly/core/frb_generated.dart'; import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
void main() { void main() {

View file

@ -1,24 +1,26 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/providers/routing.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/themes/dark.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/themes/light.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/utils/pow.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/visual/components/app_outdated.comp.dart';
import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/visual/themes/dark.dart';
import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/visual/views/home.view.dart';
import 'package:twonly/src/views/onboarding/register.view.dart'; import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/views/settings/backup/setup_backup.view.dart'; import 'package:twonly/src/visual/views/onboarding/register.view.dart';
import 'package:twonly/src/views/unlock_twonly.view.dart'; import 'package:twonly/src/visual/views/settings/backup/backup_setup.view.dart';
import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
const App({super.key}); const App({super.key});
@ -131,9 +133,9 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (_isUserCreated) { if (_isUserCreated) {
if (_isTwonlyLocked) { if (_isTwonlyLocked) {
// 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 = AppSession.currentUser.screenLockEnabled; _isTwonlyLocked = appSession.currentUser.screenLockEnabled;
} }
if (AppSession.currentUser.appVersion < 62) { if (appSession.currentUser.appVersion < 62) {
_showDatabaseMigration = true; _showDatabaseMigration = true;
} }
} }
@ -176,7 +178,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
_isTwonlyLocked = false; _isTwonlyLocked = false;
}), }),
); );
} else if (AppSession.currentUser.twonlySafeBackup == null && } else if (appSession.currentUser.twonlySafeBackup == null &&
!_skipBackup) { !_skipBackup) {
child = SetupBackupView( child = SetupBackupView(
callBack: () => setState(() { callBack: () => setState(() {
@ -204,7 +206,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
return Stack( return Stack(
children: [ children: [
child, child,
const AppOutdated(), const AppOutdatedComp(),
], ],
); );
} }

View file

@ -3,9 +3,6 @@ import 'dart:async';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/api.service.dart';
class AppEnvironment { class AppEnvironment {
static late final String cacheDir; static late final String cacheDir;
@ -29,17 +26,3 @@ class AppState {
class AppGlobalKeys { class AppGlobalKeys {
static final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); static final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
} }
class AppSession {
static late UserData currentUser;
static final _userDataUpdateController = StreamController<void>.broadcast();
static Stream<void> get onUserUpdated => _userDataUpdateController.stream;
static void triggerUserUpdate() {
_userDataUpdateController.add(null);
}
}
late ApiService apiService;
late TwonlyDB twonlyDB;

17
lib/locator.dart Normal file
View file

@ -0,0 +1,17 @@
import 'package:get_it/get_it.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/user.service.dart';
final GetIt locator = GetIt.instance;
void setupLocator() {
locator
..registerLazySingleton<UserService>(UserService.new)
..registerLazySingleton<ApiService>(ApiService.new)
..registerLazySingleton<TwonlyDB>(TwonlyDB.new);
}
UserService get appSession => locator<UserService>();
ApiService get apiService => locator<ApiService>();
TwonlyDB get twonlyDB => locator<TwonlyDB>();

View file

@ -10,21 +10,21 @@ import 'package:twonly/app.dart';
import 'package:twonly/core/bridge.dart' as bridge; import 'package:twonly/core/bridge.dart' as bridge;
import 'package:twonly/core/frb_generated.dart'; import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/callbacks.dart'; import 'package:twonly/src/callbacks/callbacks.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart';
import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/backup/create.backup.dart'; import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -34,6 +34,7 @@ void main() async {
await AppEnvironment.init(); await AppEnvironment.init();
Log.init(); Log.init();
setupLocator();
await RustLib.init(); await RustLib.init();
@ -60,7 +61,7 @@ void main() async {
} }
if (user != null) { if (user != null) {
AppSession.currentUser = user; appSession.currentUser = user;
if (user.allowErrorTrackingViaSentry) { if (user.allowErrorTrackingViaSentry) {
AppState.allowErrorTrackingViaSentry = true; AppState.allowErrorTrackingViaSentry = true;
@ -87,18 +88,15 @@ void main() async {
unawaited(setupPushNotification()); unawaited(setupPushNotification());
apiService = ApiService();
twonlyDB = TwonlyDB();
if (user != null) { if (user != null) {
if (AppSession.currentUser.appVersion < 90) { if (appSession.currentUser.appVersion < 90) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state... // BUG: Requested media files for reupload where not reuploaded because the wrong state...
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState(); await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
await updateUser((u) { await updateUser((u) {
u.appVersion = 90; u.appVersion = 90;
}); });
} }
if (AppSession.currentUser.appVersion < 91) { if (appSession.currentUser.appVersion < 91) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state... // BUG: Requested media files for reupload where not reuploaded because the wrong state...
await makeMigrationToVersion91(); await makeMigrationToVersion91();
await updateUser((u) { await updateUser((u) {

View file

@ -5,7 +5,7 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'
// ignore: implementation_imports // ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:twonly/core/bridge.dart'; import 'package:twonly/core/bridge.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';

View file

@ -1,5 +1,5 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
@ -140,7 +140,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
t.userDiscoveryVersion.isNotNull() & t.userDiscoveryVersion.isNotNull() &
t.userDiscoveryExcluded.equals(false) & t.userDiscoveryExcluded.equals(false) &
t.mediaSendCounter.isBiggerOrEqualValue( t.mediaSendCounter.isBiggerOrEqualValue(
AppSession.currentUser.minimumRequiredImagesExchanged, appSession.currentUser.minimumRequiredImagesExchanged,
), ),
)) ))
.watch(); .watch();

View file

@ -1,6 +1,6 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
@ -113,7 +113,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
int contactId, int contactId,
GroupsCompanion group, GroupsCompanion group,
) async { ) async {
final groupIdDirectChat = getUUIDforDirectChat(contactId, AppSession.currentUser.userId); final groupIdDirectChat = getUUIDforDirectChat(contactId, appSession.currentUser.userId);
final insertGroup = group.copyWith( final insertGroup = group.copyWith(
groupId: Value(groupIdDirectChat), groupId: Value(groupIdDirectChat),
isDirectChat: const Value(true), isDirectChat: const Value(true),
@ -209,7 +209,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
} }
Stream<Group?> watchDirectChat(int contactId) { Stream<Group?> watchDirectChat(int contactId) {
final groupId = getUUIDforDirectChat(contactId, AppSession.currentUser.userId); final groupId = getUUIDforDirectChat(contactId, appSession.currentUser.userId);
return (select( return (select(
groups, groups,
)..where((t) => t.groupId.equals(groupId))).watchSingleOrNull(); )..where((t) => t.groupId.equals(groupId))).watchSingleOrNull();

View file

@ -1,7 +1,7 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';

View file

@ -1,10 +1,10 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/reactions.table.dart'; import 'package:twonly/src/database/tables/reactions.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/visual/components/animate_icon.comp.dart';
part 'reactions.dao.g.dart'; part 'reactions.dao.g.dart';

View file

@ -5,7 +5,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/tables/receipts.table.dart'; import 'package:twonly/src/database/tables/receipts.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
part 'receipts.dao.g.dart'; part 'receipts.dao.g.dart';

View file

@ -1,11 +1,11 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
class ConnectIdentityKeyStore extends IdentityKeyStore { class SignalIdentityKeyStore extends IdentityKeyStore {
ConnectIdentityKeyStore(this.identityKeyPair, this.localRegistrationId); SignalIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
final IdentityKeyPair identityKeyPair; final IdentityKeyPair identityKeyPair;
final int localRegistrationId; final int localRegistrationId;

View file

@ -1,10 +1,10 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
class ConnectPreKeyStore extends PreKeyStore { class SignalPreKeyStore extends PreKeyStore {
@override @override
Future<bool> containsPreKey(int preKeyId) async { Future<bool> containsPreKey(int preKeyId) async {
final preKeyRecord = await (twonlyDB.select( final preKeyRecord = await (twonlyDB.select(

View file

@ -1,23 +1,23 @@
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/database/signal/connect_identity_key_store.dart'; import 'package:twonly/src/database/signal/signal_identity_key_store.dart';
import 'package:twonly/src/database/signal/connect_pre_key_store.dart'; import 'package:twonly/src/database/signal/signal_pre_key_store.dart';
import 'package:twonly/src/database/signal/connect_session_store.dart'; import 'package:twonly/src/database/signal/signal_session_store.dart';
import 'package:twonly/src/database/signal/connect_signed_pre_key_store.dart'; import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart';
class ConnectSignalProtocolStore implements SignalProtocolStore { class SignalSignalProtocolStore implements SignalProtocolStore {
ConnectSignalProtocolStore( SignalSignalProtocolStore(
IdentityKeyPair identityKeyPair, IdentityKeyPair identityKeyPair,
int registrationId, int registrationId,
) { ) {
_identityKeyStore = ConnectIdentityKeyStore( _identityKeyStore = SignalIdentityKeyStore(
identityKeyPair, identityKeyPair,
registrationId, registrationId,
); );
} }
final preKeyStore = ConnectPreKeyStore(); final preKeyStore = SignalPreKeyStore();
final sessionStore = ConnectSessionStore(); final sessionStore = SignalSessionStore();
final signedPreKeyStore = ConnectSignedPreKeyStore(); final signedPreKeyStore = SignalSignedPreKeyStore();
late IdentityKeyStore _identityKeyStore; late IdentityKeyStore _identityKeyStore;

View file

@ -1,9 +1,9 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
class ConnectSenderKeyStore extends SenderKeyStore { class SignalSenderKeyStore extends SenderKeyStore {
@override @override
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async { Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
final identity = final identity =

View file

@ -1,9 +1,9 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
class ConnectSessionStore extends SessionStore { class SignalSessionStore extends SessionStore {
@override @override
Future<bool> containsSession(SignalProtocolAddress address) async { Future<bool> containsSession(SignalProtocolAddress address) async {
final sessions = final sessions =

View file

@ -3,9 +3,9 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
class ConnectSignedPreKeyStore extends SignedPreKeyStore { class SignalSignedPreKeyStore extends SignedPreKeyStore {
Future<HashMap<int, Uint8List>> getStore() async { Future<HashMap<int, Uint8List>> getStore() async {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final storeSerialized = await storage.read( final storeSerialized = await storage.read(

View file

@ -3,7 +3,7 @@ import 'dart:typed_data';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'signal_identity.g.dart'; part 'signal_identity.model.g.dart';
@JsonSerializable() @JsonSerializable()
class SignalIdentity { class SignalIdentity {

View file

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'signal_identity.dart'; part of 'signal_identity.model.dart';
// ************************************************************************** // **************************************************************************
// JsonSerializableGenerator // JsonSerializableGenerator

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'userdata.g.dart'; part 'userdata.model.g.dart';
@JsonSerializable() @JsonSerializable()
class UserData { class UserData {

View file

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'userdata.dart'; part of 'userdata.model.dart';
// ************************************************************************** // **************************************************************************
// JsonSerializableGenerator // JsonSerializableGenerator

View file

@ -1,16 +1,18 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
CustomChangeProvider() { CustomChangeProvider() {
_connSub = apiService.onConnectionStateUpdated.listen( _connSub = apiService.onConnectionStateUpdated.listen(
updateConnectionState, updateConnectionState,
); );
// The API is connected before the subscription has started so ensure that the connection state is correct
_isConnected = apiService.isConnected;
} }
bool _isConnected = false; late bool _isConnected;
bool get isConnected => _isConnected;
late StreamSubscription<bool> _connSub; late StreamSubscription<bool> _connSub;
bool get isConnected => _isConnected;
@override @override
void dispose() { void dispose() {

View file

@ -1,15 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/subscription.keys.dart'; import 'package:twonly/src/constants/subscription.keys.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/purchases/purchasable_product.dart'; import 'package:twonly/src/model/purchasable_product.model.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.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/storage.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
// Gives the option to override in tests. // Gives the option to override in tests.

View file

@ -2,50 +2,50 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/app.dart'; import 'package:twonly/app.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/views/camera/camera_qr_scanner.view.dart'; import 'package:twonly/src/visual/views/camera/camera_qr_scanner.view.dart';
import 'package:twonly/src/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/visual/views/chats/add_new_user.view.dart';
import 'package:twonly/src/views/chats/archived_chats.view.dart'; import 'package:twonly/src/visual/views/chats/archived_chats.view.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/visual/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/chats/media_viewer.view.dart'; import 'package:twonly/src/visual/views/chats/media_viewer.view.dart';
import 'package:twonly/src/views/chats/start_new_chat.view.dart'; import 'package:twonly/src/visual/views/chats/start_new_chat.view.dart';
import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/visual/views/contact/contact.view.dart';
import 'package:twonly/src/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart';
import 'package:twonly/src/views/groups/group_create_select_members.view.dart'; import 'package:twonly/src/visual/views/groups/group_create_select_members.view.dart';
import 'package:twonly/src/views/onboarding/recover.view.dart'; import 'package:twonly/src/visual/views/onboarding/recover.view.dart';
import 'package:twonly/src/views/public_profile.view.dart'; import 'package:twonly/src/visual/views/public_profile.view.dart';
import 'package:twonly/src/views/settings/account.view.dart'; import 'package:twonly/src/visual/views/settings/account.view.dart';
import 'package:twonly/src/views/settings/appearance.view.dart'; import 'package:twonly/src/visual/views/settings/appearance.view.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart'; import 'package:twonly/src/visual/views/settings/backup/backup_server.view.dart';
import 'package:twonly/src/views/settings/backup/backup_server.view.dart'; import 'package:twonly/src/visual/views/settings/backup/backup_settings.view.dart';
import 'package:twonly/src/views/settings/backup/setup_backup.view.dart'; import 'package:twonly/src/visual/views/settings/backup/backup_setup.view.dart';
import 'package:twonly/src/views/settings/chat/chat_reactions.view.dart'; import 'package:twonly/src/visual/views/settings/chat/chat_reactions.view.dart';
import 'package:twonly/src/views/settings/chat/chat_settings.view.dart'; import 'package:twonly/src/visual/views/settings/chat/chat_settings.view.dart';
import 'package:twonly/src/views/settings/data_and_storage.view.dart'; import 'package:twonly/src/visual/views/settings/data_and_storage.view.dart';
import 'package:twonly/src/views/settings/data_and_storage/export_media.view.dart'; import 'package:twonly/src/visual/views/settings/data_and_storage/export_media.view.dart';
import 'package:twonly/src/views/settings/data_and_storage/import_media.view.dart'; import 'package:twonly/src/visual/views/settings/data_and_storage/import_media.view.dart';
import 'package:twonly/src/views/settings/developer/automated_testing.view.dart'; import 'package:twonly/src/visual/views/settings/developer/automated_testing.view.dart';
import 'package:twonly/src/views/settings/developer/developer.view.dart'; import 'package:twonly/src/visual/views/settings/developer/developer.view.dart';
import 'package:twonly/src/views/settings/developer/reduce_flames.view.dart'; import 'package:twonly/src/visual/views/settings/developer/reduce_flames.view.dart';
import 'package:twonly/src/views/settings/developer/retransmission_data.view.dart'; import 'package:twonly/src/visual/views/settings/developer/retransmission_data.view.dart';
import 'package:twonly/src/views/settings/help/changelog.view.dart'; import 'package:twonly/src/visual/views/settings/help/changelog.view.dart';
import 'package:twonly/src/views/settings/help/contact_us.view.dart'; import 'package:twonly/src/visual/views/settings/help/contact_us.view.dart';
import 'package:twonly/src/views/settings/help/credits.view.dart'; import 'package:twonly/src/visual/views/settings/help/credits.view.dart';
import 'package:twonly/src/views/settings/help/diagnostics.view.dart'; import 'package:twonly/src/visual/views/settings/help/diagnostics.view.dart';
import 'package:twonly/src/views/settings/help/faq.view.dart'; import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
import 'package:twonly/src/views/settings/help/faq/verifybadge.dart'; import 'package:twonly/src/visual/views/settings/help/faq/verification_bade_faq.view.dart';
import 'package:twonly/src/views/settings/help/help.view.dart'; import 'package:twonly/src/visual/views/settings/help/help.view.dart';
import 'package:twonly/src/views/settings/notification.view.dart'; import 'package:twonly/src/visual/views/settings/notification.view.dart';
import 'package:twonly/src/views/settings/privacy.view.dart'; import 'package:twonly/src/visual/views/settings/privacy.view.dart';
import 'package:twonly/src/views/settings/privacy/block_users.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart';
import 'package:twonly/src/views/settings/privacy/user_discovery.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart';
import 'package:twonly/src/views/settings/profile/modify_avatar.view.dart'; import 'package:twonly/src/visual/views/settings/profile/modify_avatar.view.dart';
import 'package:twonly/src/views/settings/profile/profile.view.dart'; import 'package:twonly/src/visual/views/settings/profile/profile.view.dart';
import 'package:twonly/src/views/settings/settings_main.view.dart'; import 'package:twonly/src/visual/views/settings/settings_main.view.dart';
import 'package:twonly/src/views/settings/share_with_friends.view.dart'; import 'package:twonly/src/visual/views/settings/share_with_friends.view.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:twonly/src/visual/views/settings/subscription/subscription.view.dart';
import 'package:twonly/src/views/user_study/user_study_questionnaire.view.dart'; import 'package:twonly/src/visual/views/user_study/user_study_questionnaire.view.dart';
import 'package:twonly/src/views/user_study/user_study_welcome.view.dart'; import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart';
final routerProvider = GoRouter( final routerProvider = GoRouter(
routes: [ routes: [
@ -175,9 +175,7 @@ final routerProvider = GoRouter(
), ),
GoRoute( GoRoute(
path: 'setup', path: 'setup',
builder: (context, state) => SetupBackupView( builder: (context, state) => const SetupBackupView(),
isPasswordChangeOnly: state.extra as bool? ?? false,
),
), ),
], ],
), ),

View file

@ -1,6 +1,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/services/user.service.dart';
class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
late ThemeMode _themeMode; late ThemeMode _themeMode;

View file

@ -18,18 +18,19 @@ import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server; as server;
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/api/server_messages.api.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart';
@ -37,12 +38,12 @@ import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/services/user_study.service.dart';
import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/keyvalue.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/storage.dart';
import 'package:twonly/src/views/user_study/user_study_data_collection.dart';
import 'package:web_socket_channel/io.dart'; import 'package:web_socket_channel/io.dart';
final lockConnecting = Mutex(); final lockConnecting = Mutex();
@ -126,7 +127,7 @@ class ApiService {
unawaited(UserDiscoveryService.checkForNewAnnouncedUsers()); unawaited(UserDiscoveryService.checkForNewAnnouncedUsers());
if (AppSession.currentUser.userStudyParticipantsToken != null) { if (appSession.currentUser.userStudyParticipantsToken != null) {
// In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection // In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection
unawaited(handleUserStudyUpload()); unawaited(handleUserStudyUpload());
} }
@ -211,7 +212,7 @@ class ApiService {
}); });
} }
bool get isConnected => _channel != null && _channel!.closeCode != null; bool get isConnected => _channel != null && _channel!.closeCode == null;
Future<void> _onDone() async { Future<void> _onDone() async {
_reconnectionDelay = 3; _reconnectionDelay = 3;

View file

@ -1,9 +1,9 @@
import 'package:clock/clock.dart' show clock; import 'package:clock/clock.dart' show clock;
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleAdditionalDataMessage( Future<void> handleAdditionalDataMessage(

View file

@ -2,12 +2,12 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart' hide Message; import 'package:twonly/src/database/twonly.db.dart' hide Message;
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';

View file

@ -1,6 +1,6 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';

View file

@ -1,12 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -161,7 +162,7 @@ Future<void> handleGroupUpdate(
case GroupActionType.demoteToMember: case GroupActionType.demoteToMember:
int? affectedContactId = update.affectedContactId.toInt(); int? affectedContactId = update.affectedContactId.toInt();
if (affectedContactId == AppSession.currentUser.userId) { if (affectedContactId == appSession.currentUser.userId) {
affectedContactId = null; affectedContactId = null;
if (actionType == GroupActionType.removedMember) { if (actionType == GroupActionType.removedMember) {
// Oh no, I just got removed from the group... // Oh no, I just got removed from the group...

View file

@ -1,14 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
hide Message; hide Message;
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';

View file

@ -1,6 +1,6 @@
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleMessageUpdate( Future<void> handleMessageUpdate(

View file

@ -1,7 +1,7 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleReaction( Future<void> handleReaction(

View file

@ -1,10 +1,10 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleTextMessage( Future<void> handleTextMessage(

View file

@ -1,8 +1,8 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -34,17 +34,18 @@ Future<void> handleUserDiscoveryRequest(
) async { ) async {
Log.info('Got a user discovery request'); Log.info('Got a user discovery request');
if (!AppSession.currentUser.isUserDiscoveryEnabled) { if (!appSession.currentUser.isUserDiscoveryEnabled) {
Log.warn('Got a user discovery request while it is disabled'); Log.warn('Got a user discovery request while it is disabled');
return; return;
} }
final contact = await twonlyDB.contactsDao.getContactById(fromUserId); final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
if (contact == null) return; if (contact == null) return;
if (contact.mediaSendCounter < AppSession.currentUser.minimumRequiredImagesExchanged || if (contact.mediaSendCounter <
appSession.currentUser.minimumRequiredImagesExchanged ||
contact.userDiscoveryExcluded) { contact.userDiscoveryExcluded) {
Log.warn( Log.warn(
'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${AppSession.currentUser.minimumRequiredImagesExchanged} or user is excluded ${contact.userDiscoveryExcluded}', 'Got a request to update user discovery, but mediaSendCounter (${contact.mediaSendCounter}) < ${appSession.currentUser.minimumRequiredImagesExchanged} or user is excluded ${contact.userDiscoveryExcluded}',
); );
return; return;
} }
@ -72,7 +73,7 @@ Future<void> handleUserDiscoveryUpdate(
int fromUserId, int fromUserId,
EncryptedContent_UserDiscoveryUpdate update, EncryptedContent_UserDiscoveryUpdate update,
) async { ) async {
if (!AppSession.currentUser.isUserDiscoveryEnabled) { if (!appSession.currentUser.isUserDiscoveryEnabled) {
Log.warn('Got a user discovery update while it is disabled'); Log.warn('Got a user discovery update while it is disabled');
return; return;
} }

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
@ -8,11 +9,11 @@ import 'package:drift/drift.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.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';
@ -96,7 +97,8 @@ Future<bool> isAllowedToDownload(MediaType type) async {
} }
final connectivityResult = await Connectivity().checkConnectivity(); final connectivityResult = await Connectivity().checkConnectivity();
final options = AppSession.currentUser.autoDownloadOptions ?? defaultAutoDownloadOptions; final options =
appSession.currentUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
if (connectivityResult.contains(ConnectivityResult.mobile)) { if (connectivityResult.contains(ConnectivityResult.mobile)) {
if (type == MediaType.video) { if (type == MediaType.video) {

View file

@ -3,11 +3,11 @@ import 'dart:async';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/backup/create.backup.dart'; import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
@ -11,15 +12,16 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.service.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -357,7 +359,7 @@ Future<void> startBackgroundMediaUpload(MediaFileService mediaService) async {
// if the user has enabled auto storing and the file // if the user has enabled auto storing and the file
// was send with unlimited counter not in twonly-Mode then store the file // was send with unlimited counter not in twonly-Mode then store the file
if (AppSession.currentUser.autoStoreAllSendUnlimitedMediaFiles && if (appSession.currentUser.autoStoreAllSendUnlimitedMediaFiles &&
!mediaService.mediaFile.requiresAuthentication && !mediaService.mediaFile.requiresAuthentication &&
!mediaService.storedPath.existsSync() && !mediaService.storedPath.existsSync() &&
mediaService.mediaFile.displayLimitInMilliseconds == null) { mediaService.mediaFile.displayLimitInMilliseconds == null) {
@ -595,7 +597,7 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
if (AppState.isInBackgroundTask || if (AppState.isInBackgroundTask ||
!connectivityResult.contains(ConnectivityResult.mobile) && !connectivityResult.contains(ConnectivityResult.mobile) &&
!connectivityResult.contains(ConnectivityResult.wifi)) { !connectivityResult.contains(ConnectivityResult.wifi)) {
// no internet, directly put it into the background... // no internet, directly put it into the background...
await FileDownloader().enqueue(task); await FileDownloader().enqueue(task);
await media.setUploadState(UploadState.backgroundUploadTaskStarted); await media.setUploadState(UploadState.backgroundUploadTaskStarted);

View file

@ -1,12 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -345,12 +346,12 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
return null; return null;
} }
} }
encryptedContent.senderProfileCounter = Int64(AppSession.currentUser.avatarCounter); encryptedContent.senderProfileCounter = Int64(appSession.currentUser.avatarCounter);
if (AppSession.currentUser.isUserDiscoveryEnabled && messageId != null) { if (appSession.currentUser.isUserDiscoveryEnabled && messageId != null) {
final contact = await twonlyDB.contactsDao.getContactById(contactId); final contact = await twonlyDB.contactsDao.getContactById(contactId);
if (contact != null && if (contact != null &&
contact.mediaSendCounter >= AppSession.currentUser.minimumRequiredImagesExchanged && contact.mediaSendCounter >= appSession.currentUser.minimumRequiredImagesExchanged &&
!contact.userDiscoveryExcluded) { !contact.userDiscoveryExcluded) {
final version = await UserDiscoveryService.getCurrentVersion(); final version = await UserDiscoveryService.getCurrentVersion();
if (version != null) { if (version != null) {
@ -406,7 +407,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
} }
Future<void> sendTypingIndication(String groupId, bool isTyping) async { Future<void> sendTypingIndication(String groupId, bool isTyping) async {
if (!AppSession.currentUser.typingIndicators) return; if (!appSession.currentUser.typingIndicators) return;
await sendCipherTextToGroup( await sendCipherTextToGroup(
groupId, groupId,
pb.EncryptedContent( pb.EncryptedContent(
@ -462,15 +463,15 @@ Future<void> notifyContactAboutOpeningMessage(
Future<void> sendContactMyProfileData(int contactId) async { Future<void> sendContactMyProfileData(int contactId) async {
List<int>? avatarSvgCompressed; List<int>? avatarSvgCompressed;
if (AppSession.currentUser.avatarSvg != null) { if (appSession.currentUser.avatarSvg != null) {
avatarSvgCompressed = gzip.encode(utf8.encode(AppSession.currentUser.avatarSvg!)); avatarSvgCompressed = gzip.encode(utf8.encode(appSession.currentUser.avatarSvg!));
} }
final encryptedContent = pb.EncryptedContent( final encryptedContent = pb.EncryptedContent(
contactUpdate: pb.EncryptedContent_ContactUpdate( contactUpdate: pb.EncryptedContent_ContactUpdate(
type: pb.EncryptedContent_ContactUpdate_Type.UPDATE, type: pb.EncryptedContent_ContactUpdate_Type.UPDATE,
avatarSvgCompressed: avatarSvgCompressed, avatarSvgCompressed: avatarSvgCompressed,
displayName: AppSession.currentUser.displayName, displayName: appSession.currentUser.displayName,
username: AppSession.currentUser.username, username: appSession.currentUser.username,
), ),
); );
await sendCipherText(contactId, encryptedContent); await sendCipherText(contactId, encryptedContent);

View file

@ -1,10 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart' hide Message; import 'package:twonly/src/database/twonly.db.dart' hide Message;
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
@ -25,7 +27,7 @@ import 'package:twonly/src/services/api/client2client/pushkeys.c2c.dart';
import 'package:twonly/src/services/api/client2client/reaction.c2c.dart'; import 'package:twonly/src/services/api/client2client/reaction.c2c.dart';
import 'package:twonly/src/services/api/client2client/text_message.c2c.dart'; import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart'; import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
@ -263,7 +265,8 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
await twonlyDB.receiptsDao.markMessagesForRetry(fromUserId); await twonlyDB.receiptsDao.markMessagesForRetry(fromUserId);
final senderProfileCounter = await checkForProfileUpdate(fromUserId, content); final senderProfileCounter = await checkForProfileUpdate(fromUserId, content);
if (AppSession.currentUser.isUserDiscoveryEnabled && content.hasSenderUserDiscoveryVersion()) { if (appSession.currentUser.isUserDiscoveryEnabled &&
content.hasSenderUserDiscoveryVersion()) {
await checkForUserDiscoveryChanges( await checkForUserDiscoveryChanges(
fromUserId, fromUserId,
content.senderUserDiscoveryVersion, content.senderUserDiscoveryVersion,
@ -351,7 +354,8 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
/// Verify that the user is (still) in that group... /// Verify that the user is (still) in that group...
if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) { if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) {
if (getUUIDforDirectChat(AppSession.currentUser.userId, fromUserId) == content.groupId) { if (getUUIDforDirectChat(appSession.currentUser.userId, fromUserId) ==
content.groupId) {
final contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId) .getContactByUserId(fromUserId)
.getSingleOrNull(); .getSingleOrNull();

View file

@ -1,6 +1,6 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
@ -11,7 +11,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart
as server; as server;
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
hide Message; hide Message;
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';

View file

@ -1,15 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/keyvalue.keys.dart'; import 'package:twonly/src/constants/keyvalue.keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/exclusive_access.dart'; import 'package:twonly/src/utils/exclusive_access.dart';
import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:workmanager/workmanager.dart'; import 'package:workmanager/workmanager.dart';
// ignore: unreachable_from_main // ignore: unreachable_from_main
@ -56,7 +56,7 @@ Future<bool> initBackgroundExecution() async {
// stay alive for multiple hours between task executions // stay alive for multiple hours between task executions
final user = await getUser(); final user = await getUser();
if (user == null) return false; if (user == null) return false;
AppSession.currentUser = user; appSession.currentUser = user;
return true; return true;
} }
@ -66,10 +66,10 @@ Future<bool> initBackgroundExecution() async {
final user = await getUser(); final user = await getUser();
if (user == null) return false; if (user == null) return false;
AppSession.currentUser = user;
twonlyDB = TwonlyDB(); setupLocator();
apiService = ApiService(); appSession.currentUser = user;
AppState.isInBackgroundTask = true; AppState.isInBackgroundTask = true;
_isInitialized = true; _isInitialized = true;

View file

@ -1,19 +1,20 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/hashlib.dart'; import 'package:hashlib/hashlib.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/services/backup/create.backup.dart'; import 'package:twonly/src/services/backup/create.backup.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/storage.dart';
Future<void> enableTwonlySafe(String password) async { Future<void> enableTwonlySafe(String password) async {
final (backupId, encryptionKey) = await getMasterKey( final (backupId, encryptionKey) = await getMasterKey(
password, password,
AppSession.currentUser.username, appSession.currentUser.username,
); );
await updateUser((user) { await updateUser((user) {
@ -65,10 +66,10 @@ Future<(Uint8List, Uint8List)> getMasterKey(
} }
String? getTwonlySafeBackupUrl() { String? getTwonlySafeBackupUrl() {
if (AppSession.currentUser.twonlySafeBackup == null) return null; if (appSession.currentUser.twonlySafeBackup == null) return null;
return getTwonlySafeBackupUrlFromServer( return getTwonlySafeBackupUrlFromServer(
AppSession.currentUser.twonlySafeBackup!.backupId, appSession.currentUser.twonlySafeBackup!.backupId,
AppSession.currentUser.backupServer, appSession.currentUser.backupServer,
); );
} }

View file

@ -2,6 +2,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
@ -11,28 +12,29 @@ import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
import 'package:twonly/src/services/backup/common.backup.dart'; import 'package:twonly/src/services/backup/common.backup.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/storage.dart';
Future<void> performTwonlySafeBackup({bool force = false}) async { Future<void> performTwonlySafeBackup({bool force = false}) async {
if (AppSession.currentUser.twonlySafeBackup == null) { if (appSession.currentUser.twonlySafeBackup == null) {
return; return;
} }
if (AppSession.currentUser.twonlySafeBackup!.backupUploadState == if (appSession.currentUser.twonlySafeBackup!.backupUploadState ==
LastBackupUploadState.pending) { LastBackupUploadState.pending) {
Log.warn('Backup upload is already pending.'); Log.warn('Backup upload is already pending.');
return; return;
} }
final lastUpdateTime = final lastUpdateTime =
AppSession.currentUser.twonlySafeBackup!.lastBackupDone; appSession.currentUser.twonlySafeBackup!.lastBackupDone;
if (!force && lastUpdateTime != null) { if (!force && lastUpdateTime != null) {
if (lastUpdateTime.isAfter(clock.now().subtract(const Duration(days: 1)))) { if (lastUpdateTime.isAfter(clock.now().subtract(const Duration(days: 1)))) {
return; return;
@ -120,8 +122,8 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes); final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
if (AppSession.currentUser.twonlySafeBackup!.lastBackupDone == null || if (appSession.currentUser.twonlySafeBackup!.lastBackupDone == null ||
AppSession.currentUser.twonlySafeBackup!.lastBackupDone!.isAfter( appSession.currentUser.twonlySafeBackup!.lastBackupDone!.isAfter(
clock.now().subtract(const Duration(days: 90)), clock.now().subtract(const Duration(days: 90)),
)) { )) {
force = true; force = true;
@ -150,7 +152,7 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
final secretBox = await chacha20.encrypt( final secretBox = await chacha20.encrypt(
backupBytes, backupBytes,
secretKey: SecretKey( secretKey: SecretKey(
AppSession.currentUser.twonlySafeBackup!.encryptionKey, appSession.currentUser.twonlySafeBackup!.encryptionKey,
), ),
nonce: nonce, nonce: nonce,
); );
@ -173,9 +175,9 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
'Create twonly Backup with a size of ${encryptedBackupBytes.length} bytes.', 'Create twonly Backup with a size of ${encryptedBackupBytes.length} bytes.',
); );
if (AppSession.currentUser.backupServer != null) { if (appSession.currentUser.backupServer != null) {
if (encryptedBackupBytes.length > if (encryptedBackupBytes.length >
AppSession.currentUser.backupServer!.maxBackupBytes) { appSession.currentUser.backupServer!.maxBackupBytes) {
Log.error('Backup is to big for the alternative backup server.'); Log.error('Backup is to big for the alternative backup server.');
await updateUser((user) { await updateUser((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed; user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;

View file

@ -2,6 +2,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
@ -9,12 +10,12 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
import 'package:twonly/src/services/backup/common.backup.dart'; import 'package:twonly/src/services/backup/common.backup.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/storage.dart';
Future<void> recoverBackup( Future<void> recoverBackup(
String username, String username,
@ -23,10 +24,7 @@ Future<void> recoverBackup(
) async { ) async {
final (backupId, encryptionKey) = await getMasterKey(password, username); final (backupId, encryptionKey) = await getMasterKey(password, username);
final backupServerUrl = await getTwonlySafeBackupUrlFromServer( final backupServerUrl = getTwonlySafeBackupUrlFromServer(backupId, server);
backupId,
server,
);
if (backupServerUrl == null) { if (backupServerUrl == null) {
Log.error('Could not create backup url'); Log.error('Could not create backup url');

View file

@ -2,12 +2,12 @@ import 'package:clock/clock.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
Future<void> syncFlameCounters({String? forceForGroup}) async { Future<void> syncFlameCounters({String? forceForGroup}) async {
final groups = await twonlyDB.groupsDao.getAllGroups(); final groups = await twonlyDB.groupsDao.getAllGroups();
@ -17,7 +17,7 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
(x) => x.totalMediaCounter == maxMessageCounter, (x) => x.totalMediaCounter == maxMessageCounter,
); );
if (AppSession.currentUser.myBestFriendGroupId != bestFriend.groupId) { if (appSession.currentUser.myBestFriendGroupId != bestFriend.groupId) {
await updateUser((user) { await updateUser((user) {
user.myBestFriendGroupId = bestFriend.groupId; user.myBestFriendGroupId = bestFriend.groupId;
}); });

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
@ -13,13 +14,13 @@ import 'package:http/http.dart' as http;
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
// ignore: implementation_imports // ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/groups.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/groups.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.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';
@ -42,8 +43,8 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
final memberIds = members.map((x) => Int64(x.userId)).toList(); final memberIds = members.map((x) => Int64(x.userId)).toList();
final groupState = EncryptedGroupState( final groupState = EncryptedGroupState(
memberIds: [Int64(AppSession.currentUser.userId)] + memberIds, memberIds: [Int64(appSession.currentUser.userId)] + memberIds,
adminIds: [Int64(AppSession.currentUser.userId)], adminIds: [Int64(appSession.currentUser.userId)],
groupName: groupName, groupName: groupName,
deleteMessagesAfterMilliseconds: Int64( deleteMessagesAfterMilliseconds: Int64(
defaultDeleteMessagesAfterMilliseconds, defaultDeleteMessagesAfterMilliseconds,
@ -283,9 +284,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
final myPubKey = keyPair.getPublicKey().serialize().toList(); final myPubKey = keyPair.getPublicKey().serialize().toList();
if (listEquals(appendedPubKey, myPubKey)) { if (listEquals(appendedPubKey, myPubKey)) {
adminIds.remove(Int64(AppSession.currentUser.userId)); adminIds.remove(Int64(appSession.currentUser.userId));
memberIds.remove( memberIds.remove(
Int64(AppSession.currentUser.userId), Int64(appSession.currentUser.userId),
); // -> Will remove the user later... ); // -> Will remove the user later...
} else { } else {
Log.info('A non admin left the group!!!'); Log.info('A non admin left the group!!!');
@ -303,7 +304,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
} }
} }
if (!memberIds.contains(Int64(AppSession.currentUser.userId))) { if (!memberIds.contains(Int64(appSession.currentUser.userId))) {
// OH no, I am no longer a member of this group... // OH no, I am no longer a member of this group...
// Return from the group... // Return from the group...
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
@ -316,7 +317,10 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
} }
final isGroupAdmin = final isGroupAdmin =
adminIds.firstWhereOrNull((t) => t.toInt() == AppSession.currentUser.userId) != null; adminIds.firstWhereOrNull(
(t) => t.toInt() == appSession.currentUser.userId,
) !=
null;
if (!listEquals(memberIds, encryptedGroupState.memberIds)) { if (!listEquals(memberIds, encryptedGroupState.memberIds)) {
if (isGroupAdmin) { if (isGroupAdmin) {
@ -368,7 +372,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
// First find and insert NEW members // First find and insert NEW members
for (final memberId in memberIds) { for (final memberId in memberIds) {
if (memberId == Int64(AppSession.currentUser.userId)) { if (memberId == Int64(appSession.currentUser.userId)) {
continue; continue;
} }
if (currentGroupMembers.any((t) => t.contactId == memberId.toInt())) { if (currentGroupMembers.any((t) => t.contactId == memberId.toInt())) {
@ -838,7 +842,9 @@ Future<bool> removeMemberFromGroup(
groupId: Value(group.groupId), groupId: Value(group.groupId),
type: const Value(GroupActionType.removedMember), type: const Value(GroupActionType.removedMember),
affectedContactId: Value( affectedContactId: Value(
removeContactId == AppSession.currentUser.userId ? null : removeContactId, removeContactId == appSession.currentUser.userId
? null
: removeContactId,
), ),
), ),
); );
@ -945,7 +951,7 @@ Future<bool> leaveAsNonAdminFromGroup(Group group) async {
EncryptedContent( EncryptedContent(
groupUpdate: EncryptedContent_GroupUpdate( groupUpdate: EncryptedContent_GroupUpdate(
groupActionType: groupActionType.name, groupActionType: groupActionType.name,
affectedContactId: Int64(AppSession.currentUser.userId), affectedContactId: Int64(appSession.currentUser.userId),
), ),
), ),
); );

View file

@ -2,23 +2,24 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/globals.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/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; 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/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/visual/views/chats/add_new_user.view.dart';
Future<bool> handleIntentUrl(BuildContext context, Uri uri) async { Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
if (!uri.scheme.startsWith('http')) return false; if (!uri.scheme.startsWith('http')) return false;
@ -32,7 +33,7 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
if (!context.mounted) return false; if (!context.mounted) return false;
if (username == AppSession.currentUser.username) { if (username == appSession.currentUser.username) {
await context.push(Routes.settingsPublicProfile); await context.push(Routes.settingsPublicProfile);
return true; return true;
} }
@ -115,7 +116,7 @@ Future<void> handleIntentMediaFile(
final newMediaService = await initializeMediaUpload( final newMediaService = await initializeMediaUpload(
type, type,
AppSession.currentUser.defaultShowTime, appSession.currentUser.defaultShowTime,
); );
if (newMediaService == null) { if (newMediaService == null) {
Log.error('Could not create new media file for intent shared file'); Log.error('Could not create new media file for intent shared file');

View file

@ -1,10 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:pro_video_editor/pro_video_editor.dart'; import 'package:pro_video_editor/pro_video_editor.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/channels/video_compression.channel.dart'; import 'package:twonly/src/channels/video_compression.channel.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';

View file

@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/compression.service.dart'; import 'package:twonly/src/services/mediafiles/compression.service.dart';
@ -237,7 +239,7 @@ class MediaFileService {
} }
if (tempPath.existsSync()) { if (tempPath.existsSync()) {
await tempPath.copy(storedPath.path); await tempPath.copy(storedPath.path);
if (AppSession.currentUser.storeMediaFilesInGallery) { if (appSession.currentUser.storeMediaFilesInGallery) {
if (mediaFile.type == MediaType.video) { if (mediaFile.type == MediaType.video) {
await saveVideoToGallery(storedPath.path); await saveVideoToGallery(storedPath.path);
} else { } else {

View file

@ -7,7 +7,7 @@ import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/localization/generated/app_localizations_de.dart'; import 'package:twonly/src/localization/generated/app_localizations_de.dart';
import 'package:twonly/src/localization/generated/app_localizations_en.dart'; import 'package:twonly/src/localization/generated/app_localizations_en.dart';

View file

@ -8,11 +8,12 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.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/storage.dart';
import '../../../firebase_options.dart'; import '../../../firebase_options.dart';
@ -73,7 +74,7 @@ Future<void> checkForTokenUpdates() async {
} }
Future<void> initFCMAfterAuthenticated({bool force = false}) async { Future<void> initFCMAfterAuthenticated({bool force = false}) async {
if (AppSession.currentUser.updateFCMToken || force) { if (appSession.currentUser.updateFCMToken || force) {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm); final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
if (storedToken != null) { if (storedToken != null) {

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
@ -9,14 +10,14 @@ import 'package:fixnum/fixnum.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.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';

View file

@ -5,7 +5,7 @@ import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:libsignal_protocol_dart/src/invalid_message_exception.dart'; import 'package:libsignal_protocol_dart/src/invalid_message_exception.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';

View file

@ -1,15 +1,16 @@
import 'dart:convert'; import 'dart:convert';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart'; import 'package:twonly/src/database/signal/signal_protocol_store.dart';
import 'package:twonly/src/model/json/signal_identity.dart'; import 'package:twonly/src/model/json/signal_identity.model.dart';
import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.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/storage.dart';
Future<IdentityKeyPair?> getSignalIdentityKeyPair() async { Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
final signalIdentity = await getSignalIdentity(); final signalIdentity = await getSignalIdentity();
@ -20,10 +21,10 @@ Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
// This function runs after the clients authenticated with the server. // This function runs after the clients authenticated with the server.
// It then checks if it should update a new session key // It then checks if it should update a new session key
Future<void> signalHandleNewServerConnection() async { Future<void> signalHandleNewServerConnection() async {
if (AppSession.currentUser.signalLastSignedPreKeyUpdated != null) { if (appSession.currentUser.signalLastSignedPreKeyUpdated != null) {
final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48)); final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48));
final isYoungerThan48Hours = final isYoungerThan48Hours =
(AppSession.currentUser.signalLastSignedPreKeyUpdated!).isAfter( (appSession.currentUser.signalLastSignedPreKeyUpdated!).isAfter(
fortyEightHoursAgo, fortyEightHoursAgo,
); );
if (isYoungerThan48Hours) { if (isYoungerThan48Hours) {
@ -104,7 +105,7 @@ Future<void> createIfNotExistsSignalIdentity() async {
final identityKeyPair = generateIdentityKeyPair(); final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true); final registrationId = generateRegistrationId(true);
final signalStore = ConnectSignalProtocolStore( final signalStore = SignalSignalProtocolStore(
identityKeyPair, identityKeyPair,
registrationId, registrationId,
); );

View file

@ -1,6 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';

View file

@ -1,21 +1,21 @@
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart'; import 'package:twonly/src/database/signal/signal_protocol_store.dart';
import 'package:twonly/src/model/json/signal_identity.dart'; import 'package:twonly/src/model/json/signal_identity.model.dart';
import 'package:twonly/src/services/signal/consts.signal.dart'; import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
Future<ConnectSignalProtocolStore?> getSignalStore() async { Future<SignalSignalProtocolStore?> getSignalStore() async {
return getSignalStoreFromIdentity((await getSignalIdentity())!); return getSignalStoreFromIdentity((await getSignalIdentity())!);
} }
Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity( Future<SignalSignalProtocolStore> getSignalStoreFromIdentity(
SignalIdentity signalIdentity, SignalIdentity signalIdentity,
) async { ) async {
final identityKeyPair = IdentityKeyPair.fromSerialized( final identityKeyPair = IdentityKeyPair.fromSerialized(
signalIdentity.identityKeyPairU8List, signalIdentity.identityKeyPairU8List,
); );
return ConnectSignalProtocolStore( return SignalSignalProtocolStore(
identityKeyPair, identityKeyPair,
signalIdentity.registrationId, signalIdentity.registrationId,
); );

View file

@ -0,0 +1,91 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mutex/mutex.dart';
import 'package:provider/provider.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart';
class UserService {
late UserData currentUser;
final _userDataUpdateController = StreamController<void>.broadcast();
Stream<void> get onUserUpdated => _userDataUpdateController.stream;
void triggerUserUpdate() {
_userDataUpdateController.add(null);
}
void dispose() {
_userDataUpdateController.close();
}
}
Future<bool> isUserCreated() async {
final user = await getUser();
if (user == null) {
return false;
}
appSession.currentUser = user;
return true;
}
Future<UserData?> getUser() async {
try {
final userJson = await const FlutterSecureStorage().read(
key: SecureStorageKeys.userData,
);
if (userJson == null) {
return null;
}
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
final user = UserData.fromJson(userMap);
return user;
} catch (e) {
Log.error('Error getting user: $e');
return null;
}
}
Future<void> updateUsersPlan(
BuildContext context,
SubscriptionPlan plan,
) async {
context.read<PurchasesProvider>().plan = plan;
await updateUser((user) {
user.subscriptionPlan = plan.name;
});
if (!context.mounted) return;
context.read<PurchasesProvider>().updatePlan(plan);
}
Mutex updateProtection = Mutex();
Future<void> updateUser(
void Function(UserData userData) updateUser,
) async {
await updateProtection.protect(() async {
final user = await getUser();
if (user == null) return;
if (user.defaultShowTime == 999999) {
// This was the old version for infinity -> change it to null
user.defaultShowTime = null;
}
updateUser(user);
await const FlutterSecureStorage().write(
key: SecureStorageKeys.userData,
value: jsonEncode(user),
);
appSession.currentUser = user;
});
appSession.triggerUserUpdate();
}

View file

@ -1,14 +1,15 @@
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:twonly/core/bridge/wrapper/user_discovery.dart'; import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.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/qr.dart'; import 'package:twonly/src/utils/qr.dart';
import 'package:twonly/src/utils/storage.dart';
class UserDiscoveryService { class UserDiscoveryService {
static Future<void> checkForNewAnnouncedUsers() async { static Future<void> checkForNewAnnouncedUsers() async {
@ -53,7 +54,7 @@ class UserDiscoveryService {
try { try {
await FlutterUserDiscovery.initializeOrUpdate( await FlutterUserDiscovery.initializeOrUpdate(
threshold: threshold, threshold: threshold,
userId: AppSession.currentUser.userId, userId: appSession.currentUser.userId,
publicKey: await getUserPublicKey(), publicKey: await getUserPublicKey(),
); );
await updateUser( await updateUser(

View file

@ -1,11 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/keyvalue.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/storage.dart';
const userStudySurveyKey = 'user_study_survey'; const userStudySurveyKey = 'user_study_survey';
@ -15,7 +15,7 @@ const surveyUrlBase = 'https://survey.twonly.org/upload.php';
Future<void> handleUserStudyUpload() async { Future<void> handleUserStudyUpload() async {
try { try {
final token = AppSession.currentUser.userStudyParticipantsToken; final token = appSession.currentUser.userStudyParticipantsToken;
if (token == null) return; if (token == null) return;
// in case the survey was taken offline try again // in case the survey was taken offline try again
@ -35,8 +35,8 @@ Future<void> handleUserStudyUpload() async {
await KeyValueStore.delete(userStudySurveyKey); await KeyValueStore.delete(userStudySurveyKey);
} }
if (AppSession.currentUser.lastUserStudyDataUpload != null && if (appSession.currentUser.lastUserStudyDataUpload != null &&
isToday(AppSession.currentUser.lastUserStudyDataUpload!)) { isToday(appSession.currentUser.lastUserStudyDataUpload!)) {
// Only send updates once a day. // Only send updates once a day.
// This enables to see if improvements to actually work. // This enables to see if improvements to actually work.
return; return;

View file

@ -1,6 +0,0 @@
import 'dart:ui';
class DefaultColors {
static const messageSelf = Color.fromARGB(255, 58, 136, 102);
static const messageOther = Color.fromARGB(233, 68, 137, 255);
}

View file

@ -1,10 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/locator.dart';
String getAvatarSvg(Uint8List avatarSvgCompressed) {
return utf8.decode(gzip.decode(avatarSvgCompressed));
}
Future<void> createPushAvatars({int? forceForUserId}) async { Future<void> createPushAvatars({int? forceForUserId}) async {
final contacts = await twonlyDB.contactsDao.getAllContacts(); final contacts = await twonlyDB.contactsDao.getAllContacts();
@ -47,13 +53,13 @@ File avatarPNGFile(int contactId) {
} }
Future<Uint8List> getUserAvatar() async { Future<Uint8List> getUserAvatar() async {
if (AppSession.currentUser.avatarSvg == null) { if (appSession.currentUser.avatarSvg == null) {
final data = await rootBundle.load('assets/images/default_avatar.png'); final data = await rootBundle.load('assets/images/default_avatar.png');
return data.buffer.asUint8List(); return data.buffer.asUint8List();
} }
final pictureInfo = await vg.loadPicture( final pictureInfo = await vg.loadPicture(
SvgStringLoader(AppSession.currentUser.avatarSvg!), SvgStringLoader(appSession.currentUser.avatarSvg!),
null, null,
); );
@ -62,7 +68,8 @@ Future<Uint8List> getUserAvatar() async {
final byteData = await image.toByteData(format: ui.ImageByteFormat.png); final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final pngBytes = byteData!.buffer.asUint8List(); final pngBytes = byteData!.buffer.asUint8List();
final file = avatarPNGFile(AppSession.currentUser.userId)..writeAsBytesSync(pngBytes); final file = avatarPNGFile(appSession.currentUser.userId)
..writeAsBytesSync(pngBytes);
pictureInfo.picture.dispose(); pictureInfo.picture.dispose();
return file.readAsBytesSync(); return file.readAsBytesSync();

View file

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
@ -11,8 +10,6 @@ import 'package:gal/gal.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
@ -131,30 +128,6 @@ String formatDuration(BuildContext context, int seconds) {
} }
} }
InputDecoration getInputDecoration(BuildContext context, String hintText) {
final primaryColor = Theme.of(context).colorScheme.primary;
return InputDecoration(
hintText: hintText,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(9),
borderSide: BorderSide(color: primaryColor),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
),
contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
);
}
Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
final result = await FlutterImageCompress.compressWithList(
imageBytes,
quality: 90,
);
return result;
}
Future<bool> authenticateUser( Future<bool> authenticateUser(
String localizedReason, { String localizedReason, {
bool force = true, bool force = true,
@ -180,27 +153,6 @@ Future<bool> authenticateUser(
return false; return false;
} }
Uint8List intToBytes(int value) {
final byteData = ByteData(4)..setInt32(0, value);
return byteData.buffer.asUint8List();
}
int bytesToInt(Uint8List bytes) {
final byteData = ByteData.sublistView(bytes);
return byteData.getInt32(0);
}
List<Uint8List>? removeLastXBytes(Uint8List original, int count) {
if (original.length < count) {
return null;
}
final newList = Uint8List(original.length - count)
..setAll(0, original.sublist(0, original.length - count));
final lastXBytes = original.sublist(original.length - count);
return [newList, lastXBytes];
}
bool isDarkMode(BuildContext context) { bool isDarkMode(BuildContext context) {
final selectedTheme = context.read<SettingsChangeProvider>().themeMode; final selectedTheme = context.read<SettingsChangeProvider>().themeMode;
@ -218,31 +170,6 @@ bool isToday(DateTime lastImageSend) {
lastImageSend.day == now.day; lastImageSend.day == now.day;
} }
InputDecoration inputTextMessageDeco(BuildContext context) {
return InputDecoration(
hintText: context.lang.chatListDetailInput,
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(color: Colors.grey, width: 2),
),
);
}
String truncateString(String input, {int maxLength = 20}) { String truncateString(String input, {int maxLength = 20}) {
if (input.length > maxLength) { if (input.length > maxLength) {
return '${input.characters.take(maxLength)}...'; return '${input.characters.take(maxLength)}...';
@ -301,35 +228,6 @@ Uint8List hexToUint8List(String hex) => Uint8List.fromList(
), ),
); );
Color getMessageColorFromType(
Message message,
MediaFile? mediaFile,
BuildContext context,
) {
Color color;
if (message.type == MessageType.restoreFlameCounter.name) {
color = Colors.orange;
} else if (message.type == MessageType.text.name) {
color = Colors.blueAccent;
} else if (mediaFile != null) {
if (mediaFile.requiresAuthentication) {
color = context.color.primary;
} else {
if (mediaFile.type == MediaType.video) {
color = const Color.fromARGB(255, 243, 33, 208);
} else if (mediaFile.type == MediaType.audio) {
color = const Color.fromARGB(255, 252, 149, 85);
} else {
color = Colors.redAccent;
}
}
} else {
return (isDarkMode(context)) ? Colors.white : Colors.black;
}
return color;
}
String getUUIDforDirectChat(int a, int b) { String getUUIDforDirectChat(int a, int b) {
if (a < 0 || b < 0) { if (a < 0 || b < 0) {
throw ArgumentError('Inputs must be non-negative integers.'); throw ArgumentError('Inputs must be non-negative integers.');
@ -385,17 +283,6 @@ String friendlyDateTime(
return '$timePart $datePart'; return '$timePart $datePart';
} }
String getAvatarSvg(Uint8List avatarSvgCompressed) {
final raw = gzip.decode(avatarSvgCompressed);
return utf8.decode(raw);
}
void printWrapped(String text) {
final pattern = RegExp('.{1,800}'); // 800 is the size of each chunk
// ignore: avoid_print
pattern.allMatches(text).forEach((match) => print(match.group(0)));
}
Future<List<int>> sha256File(File file) async { Future<List<int>> sha256File(File file) async {
final input = file.openRead(); final input = file.openRead();
final sha256Sink = AccumulatorSink<Digest>(); final sha256Sink = AccumulatorSink<Digest>();

View file

@ -1,11 +1,11 @@
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.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/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
@ -17,8 +17,8 @@ Future<Uint8List> getProfileQrCodeData() async {
final signedPreKey = (await signalStore.loadSignedPreKeys())[0]; final signedPreKey = (await signalStore.loadSignedPreKeys())[0];
final publicProfile = PublicProfile( final publicProfile = PublicProfile(
userId: Int64(AppSession.currentUser.userId), userId: Int64(appSession.currentUser.userId),
username: AppSession.currentUser.username, username: appSession.currentUser.username,
publicIdentityKey: (await signalStore.getIdentityKeyPair()) publicIdentityKey: (await signalStore.getIdentityKeyPair())
.getPublicKey() .getPublicKey()
.serialize(), .serialize(),

View file

@ -1,78 +1,5 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mutex/mutex.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart';
Future<bool> isUserCreated() async {
final user = await getUser();
if (user == null) {
return false;
}
AppSession.currentUser = user;
return true;
}
Future<UserData?> getUser() async {
try {
final userJson = await const FlutterSecureStorage().read(
key: SecureStorageKeys.userData,
);
if (userJson == null) {
return null;
}
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
final user = UserData.fromJson(userMap);
return user;
} catch (e) {
Log.error('Error getting user: $e');
return null;
}
}
Future<void> updateUsersPlan(
BuildContext context,
SubscriptionPlan plan,
) async {
context.read<PurchasesProvider>().plan = plan;
await updateUser((user) {
user.subscriptionPlan = plan.name;
});
if (!context.mounted) return;
context.read<PurchasesProvider>().updatePlan(plan);
}
Mutex updateProtection = Mutex();
Future<void> updateUser(
void Function(UserData userData) updateUser,
) async {
await updateProtection.protect(() async {
final user = await getUser();
if (user == null) return;
if (user.defaultShowTime == 999999) {
// This was the old version for infinity -> change it to null
user.defaultShowTime = null;
}
updateUser(user);
await const FlutterSecureStorage().write(
key: SecureStorageKeys.userData,
value: jsonEncode(user),
);
AppSession.currentUser = user;
});
AppSession.triggerUserUpdate();
}
Future<bool> deleteLocalUserData() async { Future<bool> deleteLocalUserData() async {
final appDir = await getApplicationSupportDirectory(); final appDir = await getApplicationSupportDirectory();

View file

@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
class FingerprintText extends StatelessWidget {
const FingerprintText(this.longString, {super.key});
final String longString;
String formatString(String input) {
final formattedString = StringBuffer();
var blockCount = 0;
for (var i = 0; i < input.length; i += 4) {
final block = input.substring(
i,
i + 4 > input.length ? input.length : i + 4,
);
formattedString.write(block);
blockCount++;
if (blockCount == 5) {
formattedString.writeln();
blockCount = 0;
} else {
formattedString.write(' ');
}
}
return formattedString.toString().trim();
}
@override
Widget build(BuildContext context) {
return SelectableText(
formatString(longString),
style: const TextStyle(fontSize: 16, color: Colors.black),
textAlign: TextAlign.center,
);
}
}

View file

@ -1,154 +0,0 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:restart_app/restart_app.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/settings/account/refund_credits.view.dart';
class AccountView extends StatefulWidget {
const AccountView({super.key});
@override
State<AccountView> createState() => _AccountViewState();
}
class _AccountViewState extends State<AccountView> {
String? formattedBallance;
bool hasRemainingBallance = false;
@override
void initState() {
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {
final ballance = await apiService.loadPlanBalance(useCache: false);
if (ballance == null || !mounted) return;
var ballanceInCents = ballance.transactions
.where(
(x) =>
x.transactionType != Response_TransactionTypes.ThanksForTesting ||
!kReleaseMode,
)
.map((a) => a.depositCents.toInt())
.sum;
if (ballanceInCents < 0) {
ballanceInCents = 0;
}
hasRemainingBallance = ballanceInCents > 0;
final myLocale = Localizations.localeOf(context);
formattedBallance = NumberFormat.currency(
locale: myLocale.toString(),
symbol: '',
decimalDigits: 2,
).format(ballanceInCents / 100);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.lang.settingsAccount),
),
body: ListView(
children: [
// ListTile(
// title: const Text('Transfer account'),
// subtitle: const Text('Coming soon'),
// onTap: () async {
// await showAlertDialog(
// context,
// 'Coming soon',
// 'This feature is not yet implemented!',
// );
// },
// ),
// const Divider(),
Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
child: const Text(
'Danger Zone',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
ListTile(
title: Text(
context.lang.settingsAccountDeleteAccount,
style: const TextStyle(color: Colors.red),
),
subtitle: (formattedBallance == null)
? Text(context.lang.settingsAccountDeleteAccountNoInternet)
: hasRemainingBallance
? Text(
context.lang.settingsAccountDeleteAccountWithBallance(
formattedBallance!,
),
)
: Text(context.lang.settingsAccountDeleteAccountNoBallance),
onTap: (formattedBallance == null)
? null
: () async {
if (hasRemainingBallance) {
final canGoNext =
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return RefundCreditsView(
formattedBalance: formattedBallance!,
);
},
),
)
as bool?;
unawaited(initAsync());
if (canGoNext == null || !canGoNext) return;
}
if (!context.mounted) return;
final ok = await showAlertDialog(
context,
context.lang.settingsAccountDeleteModalTitle,
context.lang.settingsAccountDeleteModalBody,
);
if (ok) {
final res = await apiService.deleteAccount();
if (res.isError) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Could not delete the account. Please ensure you have a internet connection!',
),
duration: Duration(seconds: 3),
),
);
return;
}
await deleteLocalUserData();
await Restart.restartApp(
notificationTitle: 'Account successfully deleted',
notificationBody: 'Click here to open the app again',
forceKill: true,
);
}
},
),
],
),
);
}
}

View file

@ -1,83 +0,0 @@
import 'package:flutter/material.dart';
// import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/views/settings/subscription_custom/voucher.view.dart';
// import 'package:url_launcher/url_launcher.dart';
class RefundCreditsView extends StatefulWidget {
const RefundCreditsView({required this.formattedBalance, super.key});
final String formattedBalance;
@override
State<RefundCreditsView> createState() => _RefundCreditsViewState();
}
class _RefundCreditsViewState extends State<RefundCreditsView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Refund Credits'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
'Remaining balance: ${widget.formattedBalance}',
textAlign: TextAlign.center,
),
),
const SizedBox(height: 20), // Space between balance and options
ListTile(
title: const Text('Create a Voucher'),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const VoucherView();
},
),
);
if (context.mounted) {
Navigator.pop(context, false);
}
},
),
// ListTile(
// title: Text("Spend to an Open Source Project"),
// onTap: () async {},
// ),
// ListTile(
// title: Text("Spend to an NGO"),
// onTap: () async {},
// ),
// ListTile(
// title: Text("Spend to twonly"),
// onTap: () async {},
// ),
// Divider(),
// ListTile(
// title: Text(
// "Learn more about your donation",
// ),
// subtitle: Text(
// "This will open our webpage which will provide you more informations where we will donate your remaining ballance if you choose this option.",
// ),
// onTap: () {
// launchUrl(Uri.parse("https://twonly.eu/de/donation/"));
// },
// trailing: FaIcon(
// FontAwesomeIcons.arrowUpRightFromSquare,
// size: 15,
// ),
// ),
],
),
),
);
}
}

View file

@ -1,259 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/utils/misc.dart';
class BackupView extends StatefulWidget {
const BackupView({super.key});
@override
State<BackupView> createState() => _BackupViewState();
}
BackupServer defaultBackupServer = BackupServer(
serverUrl: 'Default',
retentionDays: 180,
maxBackupBytes: 2097152,
);
class _BackupViewState extends State<BackupView> {
bool isLoading = false;
int activePageIdx = 0;
final PageController pageController = PageController();
@override
void initState() {
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {}
String backupStatus(LastBackupUploadState status) {
switch (status) {
case LastBackupUploadState.none:
return context.lang.backupPending;
case LastBackupUploadState.pending:
return context.lang.backupPending;
case LastBackupUploadState.failed:
return context.lang.backupFailed;
case LastBackupUploadState.success:
return context.lang.backupSuccess;
}
}
Future<void> changeTwonlySafePassword() async {
await context.push(Routes.settingsBackupSetup, extra: true);
}
@override
Widget build(BuildContext context) {
return StreamBuilder<void>(
stream: AppSession.onUserUpdated,
builder: (context, _) {
final backupServer =
AppSession.currentUser.backupServer ?? defaultBackupServer;
return Scaffold(
appBar: AppBar(
title: Text(context.lang.settingsBackup),
),
body: PageView(
controller: pageController,
onPageChanged: (index) {
setState(() {
activePageIdx = index;
});
},
children: [
BackupOption(
title: 'twonly Backup',
description: context.lang.backupTwonlySafeDesc,
bottomButton: FilledButton(
onPressed: changeTwonlySafePassword,
child: Text(context.lang.backupChangePassword),
),
child: (AppSession.currentUser.twonlySafeBackup == null)
? null
: Column(
children: [
Table(
defaultVerticalAlignment:
TableCellVerticalAlignment.middle,
children: [
...[
(
context.lang.backupServer,
(backupServer.serverUrl.contains('@'))
? backupServer.serverUrl.split('@')[1]
: backupServer.serverUrl.replaceAll(
'https://',
'',
),
),
(
context.lang.backupMaxBackupSize,
formatBytes(backupServer.maxBackupBytes),
),
(
context.lang.backupStorageRetention,
'${backupServer.retentionDays} Days',
),
(
context.lang.backupLastBackupDate,
formatDateTime(
context,
AppSession
.currentUser
.twonlySafeBackup!
.lastBackupDone,
),
),
(
context.lang.backupLastBackupSize,
formatBytes(
AppSession
.currentUser
.twonlySafeBackup!
.lastBackupSize,
),
),
(
context.lang.backupLastBackupResult,
backupStatus(
AppSession
.currentUser
.twonlySafeBackup!
.backupUploadState,
),
),
].map((pair) {
return TableRow(
children: [
TableCell(
// padding: EdgeInsets.all(4),
child: Text(pair.$1),
),
TableCell(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 4,
),
child: Text(
pair.$2,
textAlign: TextAlign.right,
),
),
),
],
);
}),
],
),
const SizedBox(height: 10),
OutlinedButton(
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
await performTwonlySafeBackup(force: true);
setState(() {
isLoading = false;
});
},
child: Text(context.lang.backupTwonlySaveNow),
),
],
),
),
BackupOption(
title: '${context.lang.backupData} (Coming Soon)',
description: context.lang.backupDataDesc,
),
],
),
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: true,
showUnselectedLabels: true,
unselectedIconTheme: IconThemeData(
color: Theme.of(
context,
).colorScheme.inverseSurface.withAlpha(150),
),
selectedIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.inverseSurface,
),
items: [
const BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.vault, size: 17),
label: 'twonly Backup',
),
BottomNavigationBarItem(
icon: const FaIcon(Icons.archive_outlined, size: 17),
label: context.lang.backupData,
),
],
onTap: (index) async {
activePageIdx = index;
await pageController.animateToPage(
index,
duration: const Duration(milliseconds: 100),
curve: Curves.bounceIn,
);
if (mounted) setState(() {});
},
currentIndex: activePageIdx,
// ),
),
);
},
);
}
}
class BackupOption extends StatelessWidget {
const BackupOption({
required this.title,
required this.description,
this.bottomButton,
super.key,
this.child,
});
final String title;
final String description;
final Widget? child;
final Widget? bottomButton;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(description),
const SizedBox(height: 8),
if (child != null) child! else Container(),
Expanded(child: Container()),
if (bottomButton != null) Center(child: bottomButton),
],
),
),
);
}
}

View file

@ -1,130 +0,0 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/settings/subscription_custom/select_payment.view.dart';
import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
class CheckoutView extends StatefulWidget {
const CheckoutView({
required this.plan,
super.key,
this.disableMonthlyOption,
});
final SubscriptionPlan plan;
final bool? disableMonthlyOption;
@override
State<CheckoutView> createState() => _CheckoutViewState();
}
class _CheckoutViewState extends State<CheckoutView> {
int checkoutInCents = 0;
bool paidMonthly = false;
bool tryAutoRenewal = true;
@override
void initState() {
super.initState();
setCheckout(init: true);
}
void setCheckout({bool init = false}) {
checkoutInCents = getPlanPrice(widget.plan, paidMonthly: paidMonthly);
if (!init) {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
final totalPrice =
'${localePrizing(context, checkoutInCents)}/${paidMonthly ? context.lang.month : context.lang.year}';
return Scaffold(
appBar: AppBar(
title: Text(context.lang.checkoutOptions),
),
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: ListView(
children: [
PlanCard(plan: widget.plan),
if (widget.disableMonthlyOption == null ||
!widget.disableMonthlyOption!)
Padding(
padding: const EdgeInsets.all(16),
child: ListTile(
title: Text(context.lang.checkoutPayYearly),
onTap: () {
paidMonthly = !paidMonthly;
setCheckout();
},
trailing: Checkbox(
value: !paidMonthly,
onChanged: (a) {
paidMonthly = !paidMonthly;
setCheckout();
},
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Card(
color: context.color.surfaceContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
context.lang.checkoutTotal,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
totalPrice,
textAlign: TextAlign.end,
),
],
),
),
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: FilledButton(
onPressed: () async {
final success =
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return SelectPaymentView(
plan: widget.plan,
payMonthly: paidMonthly,
);
},
),
)
as bool?;
if (success != null && success && context.mounted) {
Navigator.pop(context);
}
},
child: Text(context.lang.selectPaymentMethod),
),
),
const SizedBox(height: 20),
],
),
),
);
}
}

View file

@ -1,103 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
class ManageSubscriptionView extends StatefulWidget {
const ManageSubscriptionView({
required this.ballance,
required this.nextPayment,
super.key,
});
final Response_PlanBallance? ballance;
final DateTime? nextPayment;
@override
State<ManageSubscriptionView> createState() => _ManageSubscriptionViewState();
}
class _ManageSubscriptionViewState extends State<ManageSubscriptionView> {
Response_PlanBallance? ballance;
bool? autoRenewal;
@override
void initState() {
super.initState();
ballance = widget.ballance;
if (ballance != null) {
autoRenewal = ballance!.autoRenewal;
}
unawaited(initAsync(true));
}
Future<void> initAsync(bool force) async {
if (force) {
ballance = await loadPlanBalance(useCache: false);
if (ballance != null) {
autoRenewal = ballance!.autoRenewal;
}
}
setState(() {});
}
Future<void> toggleRenewalOption() async {
final res = await apiService.updatePlanOptions(!autoRenewal!);
if (res.isError) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorCodeToText(context, res.error as ErrorCode)),
),
);
}
}
await initAsync(true);
}
@override
Widget build(BuildContext context) {
final plan = context.watch<PurchasesProvider>().plan;
final myLocale = Localizations.localeOf(context);
final paidMonthly = ballance?.paymentPeriodDays == MONTHLY_PAYMENT_DAYS;
return Scaffold(
appBar: AppBar(
title: Text(context.lang.manageSubscription),
),
body: ListView(
children: [
PlanCard(plan: plan, paidMonthly: paidMonthly),
if (isPayingUser(plan)) const SizedBox(height: 20),
if (widget.nextPayment != null && isPayingUser(plan))
ListTile(
title: Text(
'${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(widget.nextPayment!)}',
),
),
if (autoRenewal != null && isPayingUser(plan))
ListTile(
title: Text(context.lang.autoRenewal),
subtitle: Text(
context.lang.autoRenewalLongDesc,
style: const TextStyle(fontSize: 12),
),
onTap: toggleRenewalOption,
trailing: Switch(
value: autoRenewal!,
onChanged: (a) async {
await toggleRenewalOption();
},
),
),
],
),
);
}
}

View file

@ -1,286 +0,0 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/subscription_custom/subscription.view.dart';
import 'package:twonly/src/views/settings/subscription_custom/voucher.view.dart';
import 'package:url_launcher/url_launcher.dart';
class SelectPaymentView extends StatefulWidget {
const SelectPaymentView({
super.key,
this.plan,
this.payMonthly,
this.valueInCents,
});
final SubscriptionPlan? plan;
final bool? payMonthly;
final int? valueInCents;
@override
State<SelectPaymentView> createState() => _SelectPaymentViewState();
}
enum PaymentMethods {
twonlyCredit,
googleSubscription,
appleSubscription,
}
class _SelectPaymentViewState extends State<SelectPaymentView> {
int? balanceInCents;
int checkoutInCents = 0;
bool tryAutoRenewal = true;
PaymentMethods paymentMethods = PaymentMethods.twonlyCredit;
@override
void initState() {
super.initState();
setCheckout(true);
unawaited(initAsync());
}
Future<void> initAsync() async {
final balance = await loadPlanBalance();
if (balance == null) {
balanceInCents = 0;
} else {
balanceInCents = balance.transactions
.map((a) => a.depositCents.toInt())
.sum;
}
setState(() {});
}
void setCheckout(bool init) {
if (widget.valueInCents != null && widget.valueInCents! > 0) {
checkoutInCents = widget.valueInCents!;
} else if (widget.plan != null) {
checkoutInCents = getPlanPrice(
widget.plan!,
paidMonthly: widget.payMonthly!,
);
} else {
/// Nothing to checkout for...
Navigator.pop(context);
}
if (!init) {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
final totalPrice = (widget.plan != null && widget.payMonthly != null)
? '${localePrizing(context, checkoutInCents)}/${(widget.payMonthly!) ? context.lang.month : context.lang.year}'
: localePrizing(context, checkoutInCents);
final canPay =
paymentMethods == PaymentMethods.twonlyCredit &&
(balanceInCents == null || balanceInCents! >= checkoutInCents);
return Scaffold(
appBar: AppBar(
title: Text(context.lang.selectPaymentMethod),
),
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.all(16),
child: Text(
context.lang.testPaymentMethod,
textAlign: TextAlign.center,
),
),
),
Expanded(
child: ListView(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Card(
color: context.color.surfaceContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.lang.twonlyCredit),
if (balanceInCents != null)
Text(
'${context.lang.currentBalance}: ${localePrizing(context, balanceInCents!)}',
style: const TextStyle(fontSize: 10),
),
],
),
Checkbox(
value:
paymentMethods == PaymentMethods.twonlyCredit,
onChanged: (value) {
setState(() {
paymentMethods = PaymentMethods.twonlyCredit;
});
},
),
],
),
),
),
),
],
),
),
if (!canPay) ...[
Padding(
padding: const EdgeInsets.all(16),
child: Text(
context.lang.notEnoughCredit,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: FilledButton(
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const VoucherView();
},
),
);
await initAsync();
},
child: Text(context.lang.chargeCredit),
),
),
],
Padding(
padding: const EdgeInsets.all(16),
child: ListTile(
title: Text(context.lang.autoRenewal),
subtitle: Text(context.lang.autoRenewalDesc),
onTap: () {
tryAutoRenewal = !tryAutoRenewal;
setCheckout(false);
},
trailing: Checkbox(
value: tryAutoRenewal,
onChanged: (a) {
tryAutoRenewal = !tryAutoRenewal;
setCheckout(false);
},
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Card(
color: context.color.surfaceContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
context.lang.checkoutTotal,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
totalPrice,
textAlign: TextAlign.end,
),
],
),
),
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: FilledButton(
onPressed: canPay
? () async {
final res = await apiService.switchToPayedPlan(
widget.plan!.name,
widget.payMonthly!,
tryAutoRenewal,
);
if (!context.mounted) return;
if (res.isSuccess) {
await updateUsersPlan(context, widget.plan!);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.planSuccessUpgraded),
),
);
Navigator.of(context).pop(true);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
errorCodeToText(
context,
res.error as ErrorCode,
),
),
),
);
}
}
: null,
child: Text(context.lang.checkoutSubmit),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () => launchUrl(
Uri.parse(
'https://twonly.eu/de/legal/#revocation-policy',
),
),
child: const Text(
'Widerrufsbelehrung',
style: TextStyle(color: Colors.blue),
),
),
TextButton(
onPressed: () => launchUrl(
Uri.parse('https://twonly.eu/de/legal/agb.html'),
),
child: const Text(
'ABG',
style: TextStyle(color: Colors.blue),
),
),
],
),
const SizedBox(height: 20),
],
),
),
);
}
}

View file

@ -1,473 +0,0 @@
// ignore_for_file: inference_failure_on_instance_creation
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/settings/subscription/additional_users.view.dart';
import 'package:twonly/src/views/settings/subscription_custom/checkout.view.dart';
import 'package:twonly/src/views/settings/subscription_custom/manage_subscription.view.dart';
import 'package:twonly/src/views/settings/subscription_custom/transaction.view.dart';
import 'package:twonly/src/views/settings/subscription_custom/voucher.view.dart';
String localePrizing(BuildContext context, int cents) {
final myLocale = Localizations.localeOf(context);
final euros = cents / 100;
if (euros == euros.toInt()) {
return '${euros.toInt()}';
}
return NumberFormat.currency(
locale: myLocale.toString(),
symbol: '',
decimalDigits: 2,
).format(cents / 100);
}
Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async {
final ballance = await apiService.getPlanBallance();
if (ballance != null) {
await updateUser((u) => u.lastPlanBallance = ballance.writeToJson());
return ballance;
}
final user = await getUser();
if (user != null && user.lastPlanBallance != null && useCache) {
try {
return Response_PlanBallance.fromJson(
user.lastPlanBallance!,
);
} catch (e) {
Log.error('from json: $e');
}
}
return ballance;
}
// ignore: constant_identifier_names
const int MONTHLY_PAYMENT_DAYS = 30;
// ignore: constant_identifier_names
const int YEARLY_PAYMENT_DAYS = 365;
class SubscriptionCustomView extends StatefulWidget {
const SubscriptionCustomView({super.key, this.redirectError});
final ErrorCode? redirectError;
@override
State<SubscriptionCustomView> createState() => _SubscriptionCustomViewState();
}
class _SubscriptionCustomViewState extends State<SubscriptionCustomView> {
bool loaded = false;
bool testerRequested = true;
Response_PlanBallance? ballance;
String? additionalOwnerName;
@override
void initState() {
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {
ballance = await loadPlanBalance();
if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) {
final ownerId = ballance!.additionalAccountOwnerId.toInt();
final contact = await twonlyDB.contactsDao
.getContactByUserId(ownerId)
.getSingleOrNull();
if (contact != null) {
additionalOwnerName = getContactDisplayName(contact);
} else {
additionalOwnerName = ownerId.toString();
}
}
setState(() {});
}
@override
Widget build(BuildContext context) {
final myLocale = Localizations.localeOf(context);
String? formattedBalance;
DateTime? nextPayment;
final currentPlan = context.watch<PurchasesProvider>().plan;
if (ballance != null) {
final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch(
ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000,
);
if (isPayingUser(currentPlan)) {
nextPayment = lastPaymentDateTime.add(
Duration(days: ballance!.paymentPeriodDays.toInt()),
);
}
final ballanceInCents = ballance!.transactions
.map((a) => a.depositCents.toInt())
.sum;
formattedBalance = NumberFormat.currency(
locale: myLocale.toString(),
symbol: '',
decimalDigits: 2,
).format(ballanceInCents / 100);
}
return Scaffold(
appBar: AppBar(
title: Text(context.lang.settingsSubscription),
),
body: ListView(
children: [
if (widget.redirectError != null)
Center(
child: Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orangeAccent,
borderRadius: BorderRadius.circular(15),
),
child: Text(
(widget.redirectError == ErrorCode.PlanLimitReached)
? context.lang.planLimitReached
: context.lang.planNotAllowed,
style: const TextStyle(color: Colors.black),
textAlign: TextAlign.center,
),
),
),
Padding(
padding: const EdgeInsets.all(32),
child: Center(
child: Container(
decoration: BoxDecoration(
color: context.color.primary,
borderRadius: BorderRadius.circular(15),
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
child: Text(
currentPlan.name,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: isDarkMode(context) ? Colors.black : Colors.white,
),
),
),
),
),
if (additionalOwnerName != null)
Center(
child: Text(
context.lang.partOfPaidPlanOf(additionalOwnerName!),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.orange),
),
),
if (!isPayingUser(currentPlan))
Center(
child: Padding(
padding: const EdgeInsets.all(18),
child: Text(
context.lang.upgradeToPaidPlan,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
),
),
if (!isPayingUser(currentPlan) ||
currentPlan == SubscriptionPlan.Tester)
PlanCard(
plan: SubscriptionPlan.Pro,
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const CheckoutView(
plan: SubscriptionPlan.Pro,
);
},
),
);
await initAsync();
},
),
if (currentPlan != SubscriptionPlan.Family)
PlanCard(
plan: SubscriptionPlan.Family,
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return CheckoutView(
plan: SubscriptionPlan.Family,
disableMonthlyOption:
currentPlan == SubscriptionPlan.Pro &&
ballance!.paymentPeriodDays.toInt() ==
YEARLY_PAYMENT_DAYS,
);
},
),
);
await initAsync();
},
),
const SizedBox(height: 10),
if (currentPlan != SubscriptionPlan.Family) const Divider(),
BetterListTile(
icon: FontAwesomeIcons.gears,
text: context.lang.manageSubscription,
subtitle: (nextPayment != null)
? Text(
'${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(nextPayment)}',
)
: null,
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ManageSubscriptionView(
ballance: ballance,
nextPayment: nextPayment,
);
},
),
);
await initAsync();
},
),
BetterListTile(
icon: FontAwesomeIcons.moneyBillTransfer,
text: context.lang.transactionHistory,
subtitle: (formattedBalance != null)
? Text('${context.lang.currentBalance}: $formattedBalance')
: null,
onTap: () async {
if (formattedBalance == null) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return TransactionView(
transactions: ballance?.transactions,
formattedBalance: formattedBalance!,
);
},
),
);
},
),
if (isPayingUser(currentPlan) ||
currentPlan == SubscriptionPlan.Tester)
BetterListTile(
icon: FontAwesomeIcons.userPlus,
text: context.lang.manageAdditionalUsers,
subtitle: loaded ? Text('${context.lang.open}: 3') : null,
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return AdditionalUsersView(
ballance: ballance,
);
},
),
);
await initAsync();
},
),
BetterListTile(
icon: FontAwesomeIcons.ticket,
text: context.lang.createOrRedeemVoucher,
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const VoucherView();
},
),
);
await initAsync();
},
),
const SizedBox(height: 30),
],
),
);
}
}
int getPlanPrice(SubscriptionPlan plan, {required bool paidMonthly}) {
switch (plan) {
case SubscriptionPlan.Pro:
return paidMonthly ? 100 : 1000;
case SubscriptionPlan.Family:
return paidMonthly ? 200 : 2000;
// ignore: no_default_cases
default:
return 0;
}
}
class PlanCard extends StatelessWidget {
const PlanCard({
required this.plan,
super.key,
this.refund,
this.onTap,
this.paidMonthly,
});
final SubscriptionPlan plan;
final void Function()? onTap;
final int? refund;
final bool? paidMonthly;
@override
Widget build(BuildContext context) {
final yearlyPrice = getPlanPrice(plan, paidMonthly: false);
final monthlyPrice = getPlanPrice(plan, paidMonthly: true);
var features = <String>[];
switch (plan.name) {
case 'Free':
features = [context.lang.freeFeature1];
case 'Plus':
features = [context.lang.plusFeature1, context.lang.plusFeature2];
case 'Tester':
case 'Pro':
features = [
context.lang.proFeature1,
context.lang.proFeature2,
context.lang.proFeature3,
context.lang.proFeature4,
];
case 'Family':
features = [
context.lang.proFeature1,
context.lang.familyFeature2,
context.lang.proFeature3,
context.lang.proFeature4,
];
default:
}
return Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: GestureDetector(
onTap: onTap,
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
color: context.color.surfaceContainer,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
plan.name,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
if (yearlyPrice != 0) const SizedBox(height: 10),
if (yearlyPrice != 0 && paidMonthly == null)
Column(
children: [
if (paidMonthly == null || paidMonthly!)
Text(
'${localePrizing(context, yearlyPrice)}/${context.lang.year}',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
if (paidMonthly == null || !paidMonthly!)
Text(
'${localePrizing(context, monthlyPrice)}/${context.lang.month}',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
if (paidMonthly != null)
Text(
(paidMonthly!)
? '${localePrizing(context, monthlyPrice)}/${context.lang.month}'
: '${localePrizing(context, yearlyPrice)}/${context.lang.year}',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 10),
...features.map(
(feature) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
feature,
textAlign: TextAlign.center,
),
),
),
if (refund != null && refund! > 0)
Padding(
padding: const EdgeInsets.only(top: 7),
child: Text(
context.lang.subscriptionRefund(
localePrizing(context, refund!),
),
textAlign: TextAlign.center,
style: TextStyle(
color: context.color.primary,
fontSize: 12,
),
),
),
if (onTap != null)
Padding(
padding: const EdgeInsets.only(top: 10),
child: FilledButton.icon(
onPressed: onTap,
label: Text(
context.lang.upgradeToPaidPlanButton(plan.name, ''),
),
),
),
],
),
),
),
),
);
}
}

View file

@ -1,147 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/utils/misc.dart';
class TransactionView extends StatefulWidget {
const TransactionView({
required this.transactions,
required this.formattedBalance,
super.key,
});
final List<Response_Transaction>? transactions;
final String formattedBalance;
@override
State<TransactionView> createState() => _TransactionViewState();
}
class _TransactionViewState extends State<TransactionView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.lang.transactionHistory),
),
body: ListView(
children: [
Padding(
padding: const EdgeInsets.all(32),
child: Center(
child: Container(
decoration: BoxDecoration(
color: context.color.primary,
borderRadius: BorderRadius.circular(15),
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
child: Text(
widget.formattedBalance,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: isDarkMode(context) ? Colors.black : Colors.white,
),
),
),
),
),
if (widget.transactions != null)
...widget.transactions!.map((x) => TransactionCard(transaction: x)),
],
),
);
}
}
class TransactionCard extends StatefulWidget {
const TransactionCard({required this.transaction, super.key});
final Response_Transaction transaction;
@override
State<TransactionCard> createState() => _TransactionCardState();
}
class _TransactionCardState extends State<TransactionCard> {
String typeToText(Response_TransactionTypes type) {
switch (type) {
case Response_TransactionTypes.Cash:
return context.lang.transactionCash;
case Response_TransactionTypes.PlanUpgrade:
return context.lang.transactionPlanUpgrade;
case Response_TransactionTypes.Refund:
return context.lang.transactionRefund;
case Response_TransactionTypes.ThanksForTesting:
return context.lang.transactionThanksForTesting;
case Response_TransactionTypes.Unknown:
return context.lang.transactionUnknown;
case Response_TransactionTypes.VoucherCreated:
return context.lang.transactionVoucherCreated;
case Response_TransactionTypes.VoucherRedeemed:
return context.lang.transactionVoucherRedeemed;
case Response_TransactionTypes.AutoRenewal:
return context.lang.transactionAutoRenewal;
}
return type.toString();
}
@override
Widget build(BuildContext context) {
final myLocale = Localizations.localeOf(context);
final formattedValue = NumberFormat.currency(
locale: myLocale.toString(),
symbol: '',
decimalDigits: 2,
).format(widget.transaction.depositCents.toInt() / 100);
final timestamp = DateTime.fromMillisecondsSinceEpoch(
widget.transaction.createdAtUnixTimestamp.toInt() * 1000,
);
return Card(
margin: const EdgeInsets.all(10),
elevation: 5,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
typeToText(widget.transaction.transactionType),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
DateFormat.yMMMMd(myLocale.toString()).format(timestamp),
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
Text(
formattedValue,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: (widget.transaction.depositCents < 0)
? Colors.red
: context.color.primary,
),
),
],
),
],
),
),
);
}
}

View file

@ -1,321 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/utils/misc.dart';
class VoucherView extends StatefulWidget {
const VoucherView({super.key});
@override
State<VoucherView> createState() => _VoucherViewState();
}
class _VoucherViewState extends State<VoucherView> {
List<Response_Voucher> vouchers = [];
@override
void initState() {
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {
final resVouchers = await apiService.getVoucherList();
setState(() {
vouchers = resVouchers?.vouchers ?? [];
});
}
@override
Widget build(BuildContext context) {
final openVoucher = vouchers.where((x) => !x.redeemed && !x.requested);
final redeemedVoucher = vouchers.where((x) => x.redeemed);
return Scaffold(
appBar: AppBar(
title: Text(context.lang.createOrRedeemVoucher),
),
body: ListView(
children: [
ListTile(
title: Text(context.lang.redeemVoucher),
onTap: () async {
await redeemVoucher(context);
await initAsync();
},
),
ListTile(
title: Text(context.lang.createVoucher),
onTap: () async {
await showBuyVoucher(context);
await initAsync();
},
),
const Divider(),
if (openVoucher.isNotEmpty)
ListTile(
title: Text(
context.lang.openVouchers,
style: const TextStyle(fontSize: 13),
),
),
...openVoucher.map((x) => VoucherCard(voucher: x)),
if (redeemedVoucher.isNotEmpty)
ListTile(
title: Text(
context.lang.redeemedVouchers,
style: const TextStyle(fontSize: 13),
),
),
...redeemedVoucher.map((x) => VoucherCard(voucher: x)),
],
),
);
}
}
class VoucherCard extends StatefulWidget {
const VoucherCard({required this.voucher, super.key});
final Response_Voucher voucher;
@override
State<VoucherCard> createState() => _VoucherCardState();
}
class _VoucherCardState extends State<VoucherCard> {
Future<void> _copyVoucherId() async {
if (!widget.voucher.redeemed) {
await Clipboard.setData(ClipboardData(text: widget.voucher.voucherId));
await HapticFeedback.heavyImpact();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${widget.voucher.voucherId} copied.')),
);
}
}
@override
Widget build(BuildContext context) {
final isRedeemed = widget.voucher.redeemed || widget.voucher.requested;
final myLocale = Localizations.localeOf(context);
final formattedValue = NumberFormat.currency(
locale: myLocale.toString(),
symbol: '',
decimalDigits: 2,
).format(widget.voucher.valueCents.toInt() / 100);
return GestureDetector(
onTap: _copyVoucherId,
child: Card(
margin: const EdgeInsets.all(10),
elevation: 5,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.voucher.voucherId.toUpperCase(),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isRedeemed ? Colors.grey : context.color.onSurface,
),
),
Text(
formattedValue,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isRedeemed ? Colors.grey : context.color.onSurface,
),
),
],
),
],
),
),
),
);
}
}
Future<void> redeemVoucher(BuildContext context) async {
var voucherCode = '';
//
// ignore: inference_failure_on_function_invocation
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(context.lang.redeemVoucher),
content: StatefulBuilder(
builder: (context, setState) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: TextField(
onChanged: (value) {
// Convert to uppercase
setState(() {
voucherCode = value.toUpperCase();
});
},
decoration: InputDecoration(
labelText: context.lang.enterVoucherCode,
border: const OutlineInputBorder(),
),
// Set the text to be uppercase
textCapitalization: TextCapitalization.characters,
),
),
],
),
);
},
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(context.lang.cancel),
),
TextButton(
onPressed: () async {
final res = await apiService.redeemVoucher(voucherCode);
if (!context.mounted) return;
if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.lang.voucherRedeemed)),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
errorCodeToText(
context,
res.error as ErrorCode,
),
),
),
);
}
Navigator.of(context).pop();
},
child: Text(context.lang.ok),
),
],
);
},
);
}
Future<void> showBuyVoucher(BuildContext context) async {
var quantity = 1000;
//
// ignore: inference_failure_on_function_invocation
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(context.lang.createVoucher),
content: StatefulBuilder(
builder: (context, setState) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(context.lang.createVoucherDesc),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () {
if (quantity > 1) {
setState(() {
if (quantity <= 100) return;
if (quantity <= 1000) {
quantity -= 100;
} else {
quantity -= 500;
}
});
}
},
),
Text(
NumberFormat.currency(
locale: Localizations.localeOf(context).toString(),
symbol: '',
decimalDigits: 2,
).format(quantity / 100),
style: const TextStyle(fontSize: 24),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
setState(() {
if (quantity >= 1000) {
quantity += 500;
} else {
quantity += 100;
}
});
},
),
],
),
],
),
);
},
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Close the dialog
},
child: Text(context.lang.cancel),
),
TextButton(
onPressed: () async {
final res = await apiService.buyVoucher(quantity);
if (!context.mounted) return;
if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.lang.voucherCreated)),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
errorCodeToText(
context,
res.error as ErrorCode,
),
),
),
);
}
Navigator.of(context).pop(); // Close the dialog
},
child: Text(context.lang.buy),
),
],
);
},
);
}

View file

@ -17,8 +17,12 @@ bool isOneEmoji(String character) {
return false; return false;
} }
class EmojiAnimation extends StatelessWidget { class EmojiAnimationComp extends StatelessWidget {
const EmojiAnimation({required this.emoji, super.key, this.repeat = true}); const EmojiAnimationComp({
required this.emoji,
super.key,
this.repeat = true,
});
final String emoji; final String emoji;
final bool repeat; final bool repeat;
static final Map<String, String> animatedIcons = { static final Map<String, String> animatedIcons = {
@ -279,7 +283,7 @@ class EmojiAnimationFlying extends StatelessWidget {
padding: EdgeInsets.only(bottom: 20 * value), padding: EdgeInsets.only(bottom: 20 * value),
child: SizedBox( child: SizedBox(
width: size + 30 * value, width: size + 30 * value,
child: EmojiAnimation(emoji: emoji), child: EmojiAnimationComp(emoji: emoji),
), ),
); );
}, },

View file

@ -1,23 +1,24 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class AppOutdated extends StatefulWidget { class AppOutdatedComp extends StatefulWidget {
const AppOutdated({super.key}); const AppOutdatedComp({super.key});
@override @override
State<AppOutdated> createState() => _AppOutdatedState(); State<AppOutdatedComp> createState() => _AppOutdatedCompState();
} }
class _AppOutdatedState extends State<AppOutdated> { class _AppOutdatedCompState extends State<AppOutdatedComp> {
bool appIsOutdated = false; bool appIsOutdated = false;
bool newDeviceRegistered = false; bool newDeviceRegistered = false;
late StreamSubscription<void> _subOutdated; late StreamSubscription<void> _subOutdated;
late StreamSubscription<void> _subNewDevice; late StreamSubscription<void> _subNewDevice;

View file

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:vector_graphics/vector_graphics.dart'; import 'package:vector_graphics/vector_graphics.dart';
class AvatarIcon extends StatefulWidget { class AvatarIcon extends StatefulWidget {
@ -93,19 +93,19 @@ class _AvatarIconState extends State<AvatarIcon> {
setState(() {}); setState(() {});
}); });
} else if (widget.myAvatar) { } else if (widget.myAvatar) {
_userSub = AppSession.onUserUpdated.listen((_) { _userSub = appSession.onUserUpdated.listen((_) {
if (mounted) { if (mounted) {
setState(() { setState(() {
if (AppSession.currentUser.avatarSvg != null) { if (appSession.currentUser.avatarSvg != null) {
_avatarSvg = AppSession.currentUser.avatarSvg; _avatarSvg = appSession.currentUser.avatarSvg;
} else { } else {
_avatarContacts = []; _avatarContacts = [];
} }
}); });
} }
}); });
if (AppSession.currentUser.avatarSvg != null) { if (appSession.currentUser.avatarSvg != null) {
_avatarSvg = AppSession.currentUser.avatarSvg; _avatarSvg = appSession.currentUser.avatarSvg;
} }
} else if (widget.contactId != null) { } else if (widget.contactId != null) {
contactStream = twonlyDB.contactsDao contactStream = twonlyDB.contactsDao

View file

@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/loader/ripple.loader.dart'; import 'package:twonly/src/visual/loader/ripple.loader.dart';
class ConnectionStatusBadge extends StatelessWidget { class ConnectionStatusComp extends StatelessWidget {
const ConnectionStatusBadge({ const ConnectionStatusComp({
required this.child, required this.child,
super.key, super.key,
}); });

View file

@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/context_menu.component.dart'; import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
class UserContextMenu extends StatelessWidget { class UserContextMenu extends StatelessWidget {
const UserContextMenu({ const UserContextMenu({

View file

@ -2,7 +2,7 @@ import 'dart:io';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart';
class EmojiPickerBottom extends StatelessWidget { class EmojiPickerBottom extends StatelessWidget {
const EmojiPickerBottom({super.key}); const EmojiPickerBottom({super.key});

View file

@ -1,8 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/visual/components/animate_icon.comp.dart';
class FlameCounterWidget extends StatefulWidget { class FlameCounterWidget extends StatefulWidget {
const FlameCounterWidget({ const FlameCounterWidget({
@ -48,7 +49,8 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
} }
if (groupId != null && group != null) { if (groupId != null && group != null) {
isBestFriend = isBestFriend =
AppSession.currentUser.myBestFriendGroupId == groupId && group.alsoBestFriend; appSession.currentUser.myBestFriendGroupId == groupId &&
group.alsoBestFriend;
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId); final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
flameCounterSub = stream.listen((counter) { flameCounterSub = stream.listen((counter) {
if (mounted) { if (mounted) {
@ -85,7 +87,7 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
), ),
SizedBox( SizedBox(
height: 15, height: 15,
child: EmojiAnimation( child: EmojiAnimationComp(
emoji: flameEmoji, emoji: flameEmoji,
), ),
), ),

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class NotificationBadge extends StatelessWidget { class NotificationBadgeComp extends StatelessWidget {
const NotificationBadge({ const NotificationBadgeComp({
required this.count, required this.count,
required this.child, required this.child,
this.backgroundColor = Colors.red, this.backgroundColor = Colors.red,

View file

@ -4,15 +4,15 @@ import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart';
import 'package:twonly/src/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart';
class SelectChatDeletionTimeListTitle extends StatefulWidget { class SelectChatDeletionTimeListTitle extends StatefulWidget {
const SelectChatDeletionTimeListTitle({ const SelectChatDeletionTimeListTitle({

View file

@ -1,13 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/globals.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/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/views/components/svg_icon.dart'; import 'package:twonly/src/visual/elements/svg_icon.element.dart';
class VerifiedShield extends StatefulWidget { class VerificationBadgeComp extends StatefulWidget {
const VerifiedShield({ const VerificationBadgeComp({
this.contact, this.contact,
this.group, this.group,
super.key, super.key,
@ -23,10 +24,10 @@ class VerifiedShield extends StatefulWidget {
final bool clickable; final bool clickable;
@override @override
State<VerifiedShield> createState() => _VerifiedShieldState(); State<VerificationBadgeComp> createState() => _VerificationBadgeCompState();
} }
class _VerifiedShieldState extends State<VerifiedShield> { class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
bool isVerified = false; bool isVerified = false;
Contact? contact; Contact? contact;

View file

@ -2,12 +2,12 @@ import 'package:drift/drift.dart' hide Column;
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:twonly/globals.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/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/views/components/context_menu.component.dart'; import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
class GroupContextMenu extends StatelessWidget { class GroupContextMenu extends StatelessWidget {
const GroupContextMenu({ const GroupContextMenu({

View file

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/context_menu/context_menu.helper.dart';
class UserContextMenu extends StatelessWidget {
const UserContextMenu({
required this.contact,
required this.child,
super.key,
});
final Widget child;
final Contact contact;
@override
Widget build(BuildContext context) {
return ContextMenu(
items: [
ContextMenuItem(
title: context.lang.contextMenuUserProfile,
onTap: () => context.push(Routes.profileContact(contact.userId)),
icon: FontAwesomeIcons.user,
),
],
child: child,
);
}
}

View file

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
InputDecoration inputTextMessageDeco(BuildContext context, String hintText) {
return InputDecoration(
hintText: hintText,
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(color: Colors.grey, width: 2),
),
);
}
InputDecoration getInputDecoration(BuildContext context, String hintText) {
final primaryColor = Theme.of(context).colorScheme.primary;
return InputDecoration(
hintText: hintText,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(9),
borderSide: BorderSide(color: primaryColor),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
),
contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
);
}

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class HeadLineComponent extends StatelessWidget { class HeadLineComp extends StatelessWidget {
const HeadLineComponent(this.text, {super.key}); const HeadLineComp(this.text, {super.key});
final String text; final String text;
@override @override

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class MediaViewSizing extends StatefulWidget { class MediaViewSizingHelper extends StatefulWidget {
const MediaViewSizing({ const MediaViewSizingHelper({
required this.child, required this.child,
super.key, super.key,
this.requiredHeight, this.requiredHeight,
@ -15,10 +15,10 @@ class MediaViewSizing extends StatefulWidget {
final Widget child; final Widget child;
@override @override
State<MediaViewSizing> createState() => _MediaViewSizingState(); State<MediaViewSizingHelper> createState() => _MediaViewSizingHelperState();
} }
class _MediaViewSizingState extends State<MediaViewSizing> { class _MediaViewSizingHelperState extends State<MediaViewSizingHelper> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var needToDownSizeImage = false; var needToDownSizeImage = false;

View file

@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
class ScreenshotImage { class ScreenshotImageHelper {
ScreenshotImage({ ScreenshotImageHelper({
this.image, this.image,
this.imageBytes, this.imageBytes,
this.imageBytesFuture, this.imageBytesFuture,
@ -46,7 +46,7 @@ class ScreenshotController {
} }
late GlobalKey _containerKey; late GlobalKey _containerKey;
Future<ScreenshotImage?> capture({double? pixelRatio}) async { Future<ScreenshotImageHelper?> capture({double? pixelRatio}) async {
try { try {
final findRenderObject = _containerKey.currentContext?.findRenderObject(); final findRenderObject = _containerKey.currentContext?.findRenderObject();
if (findRenderObject == null) { if (findRenderObject == null) {
@ -62,7 +62,7 @@ class ScreenshotController {
} }
} }
final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1); final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1);
return ScreenshotImage(image: image); return ScreenshotImageHelper(image: image);
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);
} }

View file

@ -4,18 +4,18 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
class VideoPlayerWrapper extends StatefulWidget { class VideoPlayerHelper extends StatefulWidget {
const VideoPlayerWrapper({ const VideoPlayerHelper({
required this.videoPath, required this.videoPath,
super.key, super.key,
}); });
final File videoPath; final File videoPath;
@override @override
State<VideoPlayerWrapper> createState() => _VideoPlayerWrapperState(); State<VideoPlayerHelper> createState() => _VideoPlayerHelperState();
} }
class _VideoPlayerWrapperState extends State<VideoPlayerWrapper> { class _VideoPlayerHelperState extends State<VideoPlayerHelper> {
late VideoPlayerController _controller; late VideoPlayerController _controller;
@override @override

Some files were not shown because too many files have changed in this diff Show more