Merge pull request #301 from twonlyapp/dev

- Adds crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.”
- Fixes bug when saving images to the gallery
- Multiple layout issues fixed
- Multiple bug fixes
This commit is contained in:
Tobi 2025-11-10 00:10:23 +01:00 committed by GitHub
commit 0260d552bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 513 additions and 243 deletions

View file

@ -1,5 +1,12 @@
# Changelog # Changelog
## 0.0.67
- Adds crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.”
- Fixes bug when saving images to the gallery
- Multiple layout issues fixed
- Multiple bug fixes
## 0.0.62 ## 0.0.62
- Support for groups with multiple administrators - Support for groups with multiple administrators
@ -16,7 +23,6 @@
- Improved reliability of client-to-client messaging - Improved reliability of client-to-client messaging
- Multiple bug fixes - Multiple bug fixes
## 0.0.61 ## 0.0.61
- Improving image editor when changing colors - Improving image editor when changing colors

View file

@ -191,6 +191,8 @@ PODS:
- "no_screenshot (0.0.1+4)": - "no_screenshot (0.0.1+4)":
- Flutter - Flutter
- ScreenProtectorKit (~> 1.3.1) - ScreenProtectorKit (~> 1.3.1)
- objective_c (0.0.1):
- Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
@ -208,6 +210,11 @@ PODS:
- SDWebImageWebPCoder (0.14.6): - SDWebImageWebPCoder (0.14.6):
- libwebp (~> 1.0) - libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17) - SDWebImage/Core (~> 5.17)
- Sentry/HybridSDK (8.56.2)
- sentry_flutter (9.8.0):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.56.2)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@ -275,10 +282,12 @@ DEPENDENCIES:
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- no_screenshot (from `.symlinks/plugins/no_screenshot/ios`) - no_screenshot (from `.symlinks/plugins/no_screenshot/ios`)
- objective_c (from `.symlinks/plugins/objective_c/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- restart_app (from `.symlinks/plugins/restart_app/ios`) - restart_app (from `.symlinks/plugins/restart_app/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
@ -306,6 +315,7 @@ SPEC REPOS:
- ScreenProtectorKit - ScreenProtectorKit
- SDWebImage - SDWebImage
- SDWebImageWebPCoder - SDWebImageWebPCoder
- Sentry
- sqlite3 - sqlite3
- SwiftProtobuf - SwiftProtobuf
@ -352,6 +362,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/local_auth_darwin/darwin" :path: ".symlinks/plugins/local_auth_darwin/darwin"
no_screenshot: no_screenshot:
:path: ".symlinks/plugins/no_screenshot/ios" :path: ".symlinks/plugins/no_screenshot/ios"
objective_c:
:path: ".symlinks/plugins/objective_c/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
@ -360,6 +372,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
restart_app: restart_app:
:path: ".symlinks/plugins/restart_app/ios" :path: ".symlinks/plugins/restart_app/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
@ -408,6 +422,7 @@ SPEC CHECKSUMS:
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
@ -416,6 +431,8 @@ SPEC CHECKSUMS:
ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4 ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7
sentry_flutter: 4c33648b7e83310aa1fdb1b10c5491027d9643f0
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0

View file

@ -33,3 +33,4 @@ void Function(SubscriptionPlan plan) globalCallbackUpdatePlan =
Map<String, VoidCallback> globalUserDataChangedCallBack = {}; Map<String, VoidCallback> globalUserDataChangedCallBack = {};
bool globalIsAppInBackground = true; bool globalIsAppInBackground = true;
bool globalAllowErrorTrackingViaSentry = false;

View file

@ -9,6 +9,7 @@ import 'package:flutter/services.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/app.dart'; import 'package:twonly/app.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -69,13 +70,34 @@ void main() async {
unawaited(createPushAvatars()); unawaited(createPushAvatars());
await twonlyDB.messagesDao.purgeMessageTable(); await twonlyDB.messagesDao.purgeMessageTable();
final providers = [
ChangeNotifierProvider(create: (_) => settingsController),
ChangeNotifierProvider(create: (_) => CustomChangeProvider()),
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
];
if (user != null) {
if (user.allowErrorTrackingViaSentry) {
globalAllowErrorTrackingViaSentry = true;
return SentryFlutter.init(
(options) => options
..dsn =
'https://6b24a012c85144c9b522440a1d17d01c@glitchtip.twonly.eu/4'
..tracesSampleRate = 0.01
..enableAutoSessionTracking = false,
appRunner: () => runApp(
MultiProvider(
providers: providers,
child: const App(),
),
),
);
}
}
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: providers,
ChangeNotifierProvider(create: (_) => settingsController),
ChangeNotifierProvider(create: (_) => CustomChangeProvider()),
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
],
child: const App(), child: const App(),
), ),
); );

View file

@ -322,11 +322,11 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
flameCounter += 1; flameCounter += 1;
lastFlameCounterChange = Value(timestamp); lastFlameCounterChange = Value(timestamp);
// Overwrite max flame counter either the current is bigger or the th max flame counter is older then 4 days // Overwrite max flame counter either the current is bigger or the th max flame counter is older then 4 days
if ((flameCounter + 1) >= maxFlameCounter || if (flameCounter >= maxFlameCounter ||
maxFlameCounterFrom == null || maxFlameCounterFrom == null ||
maxFlameCounterFrom maxFlameCounterFrom
.isBefore(DateTime.now().subtract(const Duration(days: 5)))) { .isBefore(DateTime.now().subtract(const Duration(days: 5)))) {
maxFlameCounter = flameCounter + 1; maxFlameCounter = flameCounter;
maxFlameCounterFrom = DateTime.now(); maxFlameCounterFrom = DateTime.now();
} }
} }

View file

@ -57,7 +57,6 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
tbl.preKeyId.equals(preKey.preKeyId), tbl.preKeyId.equals(preKey.preKeyId),
)) ))
.go(); .go();
Log.info('[PREKEY] Using prekey ${preKey.preKeyId} for $contactId');
return preKey; return preKey;
} }
return null; return null;
@ -68,7 +67,6 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
List<SignalContactPreKeysCompanion> preKeys, List<SignalContactPreKeysCompanion> preKeys,
) async { ) async {
for (final preKey in preKeys) { for (final preKey in preKeys) {
Log.info('[PREKEY] Inserting others ${preKey.preKeyId}');
try { try {
await into(signalContactPreKeys).insert(preKey); await into(signalContactPreKeys).insert(preKey);
} catch (e) { } catch (e) {

View file

@ -23,14 +23,12 @@ class ConnectPreKeyStore extends PreKeyStore {
'[PREKEY] No such preKey record! - $preKeyId', '[PREKEY] No such preKey record! - $preKeyId',
); );
} }
Log.info('[PREKEY] Contact used my preKey $preKeyId');
final preKey = preKeyRecord.first.preKey; final preKey = preKeyRecord.first.preKey;
return PreKeyRecord.fromBuffer(preKey); return PreKeyRecord.fromBuffer(preKey);
} }
@override @override
Future<void> removePreKey(int preKeyId) async { Future<void> removePreKey(int preKeyId) async {
Log.info('[PREKEY] Removing $preKeyId from my own storage.');
await (twonlyDB.delete(twonlyDB.signalPreKeyStores) await (twonlyDB.delete(twonlyDB.signalPreKeyStores)
..where((tbl) => tbl.preKeyId.equals(preKeyId))) ..where((tbl) => tbl.preKeyId.equals(preKeyId)))
.go(); .go();
@ -43,7 +41,6 @@ class ConnectPreKeyStore extends PreKeyStore {
preKey: Value(record.serialize()), preKey: Value(record.serialize()),
); );
Log.info('[PREKEY] Storing $preKeyId from my own storage.');
try { try {
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion); await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion);
} catch (e) { } catch (e) {

View file

@ -818,5 +818,9 @@
"deleteChatAfterAMonth": "einem Monat.", "deleteChatAfterAMonth": "einem Monat.",
"deleteChatAfterAYear": "einem Jahr.", "deleteChatAfterAYear": "einem Jahr.",
"yourTwonlyScore": "Dein twonly-Score", "yourTwonlyScore": "Dein twonly-Score",
"registrationClosed": "Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal." "registrationClosed": "Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal.",
"dialogAskDeleteMediaFilePopTitle": "Bist du sicher, dass du dein Meisterwerk löschen möchtest?",
"dialogAskDeleteMediaFilePopDelete": "Löschen",
"allowErrorTracking": "Fehler und Crashes mit uns teilen",
"allowErrorTrackingSubtitle": "Wenn twonly abstürzt oder Fehler auftreten, werden diese automatisch an unsere selbst gehostete Glitchtip-Instanz gemeldet. Persönliche Daten wie Nachrichten oder Bilder werden niemals hochgeladen."
} }

View file

@ -596,5 +596,9 @@
"deleteChatAfterAMonth": "one month.", "deleteChatAfterAMonth": "one month.",
"deleteChatAfterAYear": "one year.", "deleteChatAfterAYear": "one year.",
"yourTwonlyScore": "Your twonly-Score", "yourTwonlyScore": "Your twonly-Score",
"registrationClosed": "Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days." "registrationClosed": "Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.",
"dialogAskDeleteMediaFilePopTitle": "Are you sure you want to delete your masterpiece?",
"dialogAskDeleteMediaFilePopDelete": "Delete",
"allowErrorTracking": "Share errors and crashes with us",
"allowErrorTrackingSubtitle": "If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded."
} }

View file

@ -2683,6 +2683,30 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'** /// **'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'**
String get registrationClosed; String get registrationClosed;
/// No description provided for @dialogAskDeleteMediaFilePopTitle.
///
/// In en, this message translates to:
/// **'Are you sure you want to delete your masterpiece?'**
String get dialogAskDeleteMediaFilePopTitle;
/// No description provided for @dialogAskDeleteMediaFilePopDelete.
///
/// In en, this message translates to:
/// **'Delete'**
String get dialogAskDeleteMediaFilePopDelete;
/// No description provided for @allowErrorTracking.
///
/// In en, this message translates to:
/// **'Share errors and crashes with us'**
String get allowErrorTracking;
/// No description provided for @allowErrorTrackingSubtitle.
///
/// In en, this message translates to:
/// **'If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded.'**
String get allowErrorTrackingSubtitle;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -1481,4 +1481,18 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get registrationClosed => String get registrationClosed =>
'Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal.'; 'Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal.';
@override
String get dialogAskDeleteMediaFilePopTitle =>
'Bist du sicher, dass du dein Meisterwerk löschen möchtest?';
@override
String get dialogAskDeleteMediaFilePopDelete => 'Löschen';
@override
String get allowErrorTracking => 'Fehler und Crashes mit uns teilen';
@override
String get allowErrorTrackingSubtitle =>
'Wenn twonly abstürzt oder Fehler auftreten, werden diese automatisch an unsere selbst gehostete Glitchtip-Instanz gemeldet. Persönliche Daten wie Nachrichten oder Bilder werden niemals hochgeladen.';
} }

View file

@ -1471,4 +1471,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get registrationClosed => String get registrationClosed =>
'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'; 'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.';
@override
String get dialogAskDeleteMediaFilePopTitle =>
'Are you sure you want to delete your masterpiece?';
@override
String get dialogAskDeleteMediaFilePopDelete => 'Delete';
@override
String get allowErrorTracking => 'Share errors and crashes with us';
@override
String get allowErrorTrackingSubtitle =>
'If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded.';
} }

View file

@ -48,9 +48,6 @@ class UserData {
int? defaultShowTime; int? defaultShowTime;
@JsonKey(defaultValue: true)
bool useHighQuality = true;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool requestedAudioPermission = false; bool requestedAudioPermission = false;
@ -73,6 +70,9 @@ class UserData {
DateTime? signalLastSignedPreKeyUpdated; DateTime? signalLastSignedPreKeyUpdated;
@JsonKey(defaultValue: false)
bool allowErrorTrackingViaSentry = false;
// -- Custom DATA -- // -- Custom DATA --
@JsonKey(defaultValue: 100_000) @JsonKey(defaultValue: 100_000)

View file

@ -26,7 +26,6 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
$enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
ThemeMode.system ThemeMode.system
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..useHighQuality = json['useHighQuality'] as bool? ?? true
..requestedAudioPermission = ..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false json['requestedAudioPermission'] as bool? ?? false
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
@ -50,6 +49,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
json['signalLastSignedPreKeyUpdated'] == null json['signalLastSignedPreKeyUpdated'] == null
? null ? null
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String) : DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..allowErrorTrackingViaSentry =
json['allowErrorTrackingViaSentry'] as bool? ?? false
..currentPreKeyIndexStart = ..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart = ..currentSignedPreKeyIndexStart =
@ -84,7 +85,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'todaysImageCounter': instance.todaysImageCounter, 'todaysImageCounter': instance.todaysImageCounter,
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'defaultShowTime': instance.defaultShowTime, 'defaultShowTime': instance.defaultShowTime,
'useHighQuality': instance.useHighQuality,
'requestedAudioPermission': instance.requestedAudioPermission, 'requestedAudioPermission': instance.requestedAudioPermission,
'showFeedbackShortcut': instance.showFeedbackShortcut, 'showFeedbackShortcut': instance.showFeedbackShortcut,
'preSelectedEmojies': instance.preSelectedEmojies, 'preSelectedEmojies': instance.preSelectedEmojies,
@ -96,6 +96,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'myBestFriendGroupId': instance.myBestFriendGroupId, 'myBestFriendGroupId': instance.myBestFriendGroupId,
'signalLastSignedPreKeyUpdated': 'signalLastSignedPreKeyUpdated':
instance.signalLastSignedPreKeyUpdated?.toIso8601String(), instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash, 'lastChangeLogHash': instance.lastChangeLogHash,

View file

@ -248,7 +248,7 @@ Future<void> requestMediaReupload(String mediaId) async {
Future<void> handleEncryptedFile(String mediaId) async { Future<void> handleEncryptedFile(String mediaId) async {
final mediaService = await MediaFileService.fromMediaId(mediaId); final mediaService = await MediaFileService.fromMediaId(mediaId);
if (mediaService == null) { if (mediaService == null) {
Log.error('Media file $mediaId not found in database.'); Log.error('Media file not found in database.');
return; return;
} }
@ -263,7 +263,7 @@ Future<void> handleEncryptedFile(String mediaId) async {
try { try {
encryptedBytes = await mediaService.encryptedPath.readAsBytes(); encryptedBytes = await mediaService.encryptedPath.readAsBytes();
} catch (e) { } catch (e) {
Log.error('Could not read encrypted media file: $mediaId. $e'); Log.error('Could not read encrypted media file: $e');
await requestMediaReupload(mediaId); await requestMediaReupload(mediaId);
return; return;
} }

View file

@ -44,14 +44,14 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
if (receipt == null) { if (receipt == null) {
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
if (receipt == null) { if (receipt == null) {
Log.error('Receipt $receiptId not found.'); Log.error('Receipt not found.');
return null; return null;
} }
} }
receiptId = receipt.receiptId; receiptId = receipt.receiptId;
if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) { if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) {
Log.error('$receiptId message already uploaded!'); Log.error('message already uploaded!');
return null; return null;
} }

View file

@ -231,15 +231,14 @@ class MediaFileService {
), ),
); );
if (originalPath.existsSync()) { if (originalPath.existsSync() && !tempPath.existsSync()) {
await originalPath.copy(tempPath.path);
await compressMedia(); await compressMedia();
} }
if (tempPath.existsSync()) { if (tempPath.existsSync()) {
await tempPath.copy(storedPath.path); await tempPath.copy(storedPath.path);
} else { } else {
Log.error( Log.error(
'Could not store image neither tempPath nor originalPath exists.', 'Could not store image neither as tempPath does not exists.',
); );
} }
unawaited(createThumbnail()); unawaited(createThumbnail());

View file

@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart';
void initLogger() { void initLogger() {
// Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; // Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL;
@ -20,6 +22,13 @@ void initLogger() {
class Log { class Log {
static void error(Object? message, [Object? error, StackTrace? stackTrace]) { static void error(Object? message, [Object? error, StackTrace? stackTrace]) {
if (globalAllowErrorTrackingViaSentry) {
try {
throw Exception(message);
} catch (exception, stackTrace) {
Sentry.captureException(exception, stackTrace: stackTrace);
}
}
Logger(_getCallerSourceCodeFilename()).shout(message, error, stackTrace); Logger(_getCallerSourceCodeFilename()).shout(message, error, stackTrace);
} }
@ -45,6 +54,15 @@ Future<String> loadLogFile() async {
} }
} }
Future<String> readLast1000Lines() async {
final dir = await getApplicationSupportDirectory();
final file = File('${dir.path}/app.log');
if (!file.existsSync()) return '';
final all = await file.readAsLines();
final start = all.length > 1000 ? all.length - 1000 : 0;
return all.sublist(start).join('\n');
}
Future<void> _writeLogToFile(LogRecord record) async { Future<void> _writeLogToFile(LogRecord record) async {
final directory = await getApplicationSupportDirectory(); final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log'); final logFile = File('${directory.path}/app.log');

View file

@ -39,6 +39,7 @@ Future<String?> saveImageToGallery(Uint8List imageBytes) async {
await Gal.putImageBytes(jpgImages); await Gal.putImageBytes(jpgImages);
return null; return null;
} on GalException catch (e) { } on GalException catch (e) {
Log.error(e);
return e.type.message; return e.type.message;
} }
} }
@ -52,6 +53,7 @@ Future<String?> saveVideoToGallery(String videoPath) async {
await Gal.putVideo(videoPath); await Gal.putVideo(videoPath);
return null; return null;
} on GalException catch (e) { } on GalException catch (e) {
Log.error(e);
return e.type.message; return e.type.message;
} }
} }

View file

@ -59,6 +59,10 @@ Future<UserData?> updateUserdata(
final userData = await updateProtection.protect<UserData?>(() async { final userData = await updateProtection.protect<UserData?>(() async {
final user = await getUser(); final user = await getUser();
if (user == null) return null; if (user == null) return null;
if (user.defaultShowTime == 999999) {
// This was the old version for infinity -> change it to null
user.defaultShowTime = null;
}
final updated = updateUser(user); final updated = updateUser(user);
await const FlutterSecureStorage() await const FlutterSecureStorage()
.write(key: SecureStorageKeys.userData, value: jsonEncode(updated)); .write(key: SecureStorageKeys.userData, value: jsonEncode(updated));

View file

@ -20,6 +20,8 @@ class HomeViewCameraPreview extends StatelessWidget {
} }
return Positioned.fill( return Positioned.fill(
child: MediaViewSizing( child: MediaViewSizing(
requiredHeight: 90,
bottomNavigation: Container(),
child: Screenshot( child: Screenshot(
controller: screenshotController, controller: screenshotController,
child: AspectRatio( child: AspectRatio(
@ -58,6 +60,8 @@ class SendToCameraPreview extends StatelessWidget {
} }
return Positioned.fill( return Positioned.fill(
child: MediaViewSizing( child: MediaViewSizing(
requiredHeight: 90,
bottomNavigation: Container(),
child: Screenshot( child: Screenshot(
controller: screenshotController, controller: screenshotController,
child: AspectRatio( child: AspectRatio(

View file

@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.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/misc.dart'; import 'package:twonly/src/utils/misc.dart';
class SaveToGalleryButton extends StatefulWidget { class SaveToGalleryButton extends StatefulWidget {
@ -54,22 +55,24 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
final storedMediaPath = widget.mediaService.storedPath; final storedMediaPath = widget.mediaService.storedPath;
final storeToGallery = gUser.storeMediaFilesInGallery;
await widget.mediaService.storeMediaFile(); await widget.mediaService.storeMediaFile();
if (storeToGallery) { if (gUser.storeMediaFilesInGallery) {
res = await saveVideoToGallery(storedMediaPath.path); if (widget.mediaService.mediaFile.type == MediaType.video) {
res = await saveVideoToGallery(storedMediaPath.path);
} else {
res = await saveImageToGallery(
storedMediaPath.readAsBytesSync(),
);
}
} }
await widget.mediaService.compressMedia();
await widget.mediaService.createThumbnail();
if (res == null) { if (res == null) {
setState(() { setState(() {
_imageSaved = true; _imageSaved = true;
}); });
} else if (mounted && context.mounted) { } else if (mounted && context.mounted) {
Log.error('Could not store media file in the gallery.');
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(res), content: Text(res),

View file

@ -206,6 +206,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
FlutterVolumeController.addListener( FlutterVolumeController.addListener(
(volume) async { (volume) async {
if (!widget.isVisible) {
await deInitVolumeControl();
return;
}
if (startedVolume == null) { if (startedVolume == null) {
startedVolume = volume; startedVolume = volume;
return; return;
@ -221,7 +225,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
if (Platform.isAndroid) { if (Platform.isAndroid) {
androidVolumeDownSub = FlutterAndroidVolumeKeydown.stream.listen((event) { androidVolumeDownSub = FlutterAndroidVolumeKeydown.stream.listen((event) {
takePicture(); if (widget.isVisible) {
takePicture();
} else {
deInitVolumeControl();
return;
}
}); });
} }
} }
@ -581,6 +590,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
return Container(); return Container();
} }
return MediaViewSizing( return MediaViewSizing(
requiredHeight: 90,
bottomNavigation: Container(),
child: GestureDetector( child: GestureDetector(
onPanStart: (details) async { onPanStart: (details) async {
if (isFront) { if (isFront) {

View file

@ -75,7 +75,7 @@ class _FilterLayerState extends State<FilterLayer> {
List<Widget> pages = [ List<Widget> pages = [
const FilterSkeleton(), const FilterSkeleton(),
const DateTimeFilter(), const DateTimeFilter(),
const LocationFilter(), // const LocationFilter(),
const FilterSkeleton(), const FilterSkeleton(),
]; ];

View file

@ -62,6 +62,27 @@ class _TextViewState extends State<TextLayer> {
}); });
} }
Future<void> onEditionComplete() async {
Future.delayed(const Duration(milliseconds: 10), () async {
setState(() {
widget.layerData.isDeleted = textController.text == '';
widget.layerData.isEditing = false;
widget.layerData.text = textController.text;
});
if (!mounted) return;
await context
.read<ImageEditorProvider>()
.updateSomeTextViewIsAlreadyEditing(false);
if (widget.onUpdate != null) {
widget.onUpdate!();
}
});
}
double maxBottomInset = 0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.layerData.isDeleted) return Container(); if (widget.layerData.isDeleted) return Container();
@ -69,6 +90,16 @@ class _TextViewState extends State<TextLayer> {
final bottom = MediaQuery.of(context).viewInsets.bottom + final bottom = MediaQuery.of(context).viewInsets.bottom +
MediaQuery.of(context).viewPadding.bottom; MediaQuery.of(context).viewPadding.bottom;
if (maxBottomInset > bottom) {
maxBottomInset = 0;
if (widget.layerData.isEditing) {
widget.layerData.isEditing = false;
onEditionComplete();
}
} else {
maxBottomInset = bottom;
}
if (widget.layerData.isEditing) { if (widget.layerData.isEditing) {
return Positioned( return Positioned(
bottom: bottom - localBottom, bottom: bottom - localBottom,
@ -83,20 +114,7 @@ class _TextViewState extends State<TextLayer> {
autofocus: true, autofocus: true,
maxLines: null, maxLines: null,
minLines: 1, minLines: 1,
onEditingComplete: () async { onEditingComplete: onEditionComplete,
setState(() {
widget.layerData.isDeleted = textController.text == '';
widget.layerData.isEditing = false;
widget.layerData.text = textController.text;
});
await context
.read<ImageEditorProvider>()
.updateSomeTextViewIsAlreadyEditing(false);
if (widget.onUpdate != null) {
widget.onUpdate!();
}
},
onTapOutside: (a) async { onTapOutside: (a) async {
widget.layerData.text = textController.text; widget.layerData.text = textController.text;
Future.delayed(const Duration(milliseconds: 100), () async { Future.delayed(const Duration(milliseconds: 100), () async {

View file

@ -264,6 +264,40 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
]; ];
} }
Future<bool?> _showBackDialog() {
return showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
context.lang.dialogAskDeleteMediaFilePopTitle,
),
actions: [
FilledButton(
child: Text(context.lang.dialogAskDeleteMediaFilePopDelete),
onPressed: () {
Navigator.pop(context, true);
},
),
TextButton(
child: Text(context.lang.cancel),
onPressed: () {
Navigator.pop(context, false);
},
),
],
);
},
);
}
Future<void> askToCloseThenClose() async {
final shouldPop = await _showBackDialog() ?? false;
if (mounted && shouldPop) {
Navigator.pop(context);
}
}
List<Widget> get actionsAtTheTop { List<Widget> get actionsAtTheTop {
if (layers.isNotEmpty && if (layers.isNotEmpty &&
layers.last.isEditing && layers.last.isEditing &&
@ -275,7 +309,14 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
FontAwesomeIcons.xmark, FontAwesomeIcons.xmark,
tooltipText: context.lang.close, tooltipText: context.lang.close,
onPressed: () async { onPressed: () async {
Navigator.pop(context, false); final nonImageFilterLayer = layers.where(
(x) => x is! BackgroundLayerData && x is! FilterLayerData,
);
if (nonImageFilterLayer.isEmpty) {
Navigator.pop(context, false);
} else {
await askToCloseThenClose();
}
}, },
), ),
Expanded(child: Container()), Expanded(child: Container()),
@ -446,153 +487,161 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
pixelRatio = MediaQuery.of(context).devicePixelRatio; pixelRatio = MediaQuery.of(context).devicePixelRatio;
return Scaffold( return PopScope<bool?>(
backgroundColor: canPop: false,
widget.sharedFromGallery ? null : Colors.white.withAlpha(0), onPopInvokedWithResult: (bool didPop, bool? result) async {
resizeToAvoidBottomInset: false, if (didPop) return;
body: Stack( await askToCloseThenClose();
fit: StackFit.expand, },
children: [ child: Scaffold(
GestureDetector( backgroundColor:
onTapDown: (details) { widget.sharedFromGallery ? null : Colors.white.withAlpha(0),
if (details.globalPosition.dy > 60) { resizeToAvoidBottomInset: false,
tabDownPosition = details.globalPosition.dy - 60; body: Stack(
} else { fit: StackFit.expand,
tabDownPosition = details.globalPosition.dy; children: [
} GestureDetector(
}, onTapDown: (details) {
onTap: () { if (details.globalPosition.dy > 60) {
if (layers.any((x) => x.isEditing)) { tabDownPosition = details.globalPosition.dy - 60;
return; } else {
} tabDownPosition = details.globalPosition.dy;
layers = layers.where((x) => !x.isDeleted).toList(); }
undoLayers.clear(); },
removedLayers.clear(); onTap: () {
layers.add( if (layers.any((x) => x.isEditing)) {
TextLayerData( return;
offset: Offset(0, tabDownPosition), }
textLayersBefore: layers.whereType<TextLayerData>().length, layers = layers.where((x) => !x.isDeleted).toList();
), undoLayers.clear();
); removedLayers.clear();
setState(() {}); layers.add(
}, TextLayerData(
child: MediaViewSizing( offset: Offset(0, tabDownPosition),
bottomNavigation: ColoredBox( textLayersBefore: layers.whereType<TextLayerData>().length,
color: Theme.of(context).colorScheme.surface, ),
child: Row( );
mainAxisAlignment: MainAxisAlignment.center, setState(() {});
children: [ },
SaveToGalleryButton( child: MediaViewSizing(
storeImageAsOriginal: storeImageAsOriginal, requiredHeight: 90,
mediaService: mediaService, bottomNavigation: ColoredBox(
displayButtonLabel: widget.sendToGroup == null, color: Theme.of(context).colorScheme.surface,
isLoading: loadingImage, child: Row(
), mainAxisAlignment: MainAxisAlignment.center,
if (widget.sendToGroup != null) const SizedBox(width: 10), children: [
if (widget.sendToGroup != null) SaveToGalleryButton(
OutlinedButton( storeImageAsOriginal: storeImageAsOriginal,
style: OutlinedButton.styleFrom( mediaService: mediaService,
iconColor: Theme.of(context).colorScheme.primary, displayButtonLabel: widget.sendToGroup == null,
foregroundColor: isLoading: loadingImage,
Theme.of(context).colorScheme.primary,
),
onPressed: pushShareImageView,
child: const FaIcon(FontAwesomeIcons.userPlus),
), ),
SizedBox(width: widget.sendToGroup == null ? 20 : 10), if (widget.sendToGroup != null) const SizedBox(width: 10),
FilledButton.icon( if (widget.sendToGroup != null)
icon: sendingOrLoadingImage OutlinedButton(
? SizedBox( style: OutlinedButton.styleFrom(
height: 12, iconColor: Theme.of(context).colorScheme.primary,
width: 12, foregroundColor:
child: CircularProgressIndicator( Theme.of(context).colorScheme.primary,
strokeWidth: 2, ),
color: Theme.of(context) onPressed: pushShareImageView,
.colorScheme child: const FaIcon(FontAwesomeIcons.userPlus),
.inversePrimary, ),
), SizedBox(width: widget.sendToGroup == null ? 20 : 10),
) FilledButton.icon(
: const FaIcon(FontAwesomeIcons.solidPaperPlane), icon: sendingOrLoadingImage
onPressed: () async { ? SizedBox(
if (sendingOrLoadingImage) return; height: 12,
if (widget.sendToGroup == null) { width: 12,
return pushShareImageView(); child: CircularProgressIndicator(
} strokeWidth: 2,
await sendImageToSinglePerson(); color: Theme.of(context)
}, .colorScheme
style: ButtonStyle( .inversePrimary,
padding: WidgetStateProperty.all<EdgeInsets>( ),
const EdgeInsets.symmetric( )
vertical: 10, : const FaIcon(FontAwesomeIcons.solidPaperPlane),
horizontal: 30, onPressed: () async {
if (sendingOrLoadingImage) return;
if (widget.sendToGroup == null) {
return pushShareImageView();
}
await sendImageToSinglePerson();
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric(
vertical: 10,
horizontal: 30,
),
), ),
), ),
label: Text(
(widget.sendToGroup == null)
? context.lang.shareImagedEditorShareWith
: substringBy(widget.sendToGroup!.groupName, 15),
style: const TextStyle(fontSize: 17),
),
), ),
label: Text( ],
(widget.sendToGroup == null) ),
? context.lang.shareImagedEditorShareWith
: substringBy(widget.sendToGroup!.groupName, 15),
style: const TextStyle(fontSize: 17),
),
),
],
), ),
), child: SizedBox(
child: SizedBox( height: currentImage.height / pixelRatio,
height: currentImage.height / pixelRatio, width: currentImage.width / pixelRatio,
width: currentImage.width / pixelRatio, child: Stack(
child: Stack( children: [
children: [ if (videoController != null)
if (videoController != null) Positioned.fill(
Positioned.fill( child: VideoPlayer(videoController!),
child: VideoPlayer(videoController!), ),
), Screenshot(
Screenshot( controller: screenshotController,
controller: screenshotController, child: LayersViewer(
child: LayersViewer( layers: layers.where((x) => !x.isDeleted).toList(),
layers: layers.where((x) => !x.isDeleted).toList(), onUpdate: () {
onUpdate: () { for (final layer in layers) {
for (final layer in layers) { layer.isEditing = false;
layer.isEditing = false; if (layer.isDeleted) {
if (layer.isDeleted) { removedLayers.add(layer);
removedLayers.add(layer); }
} }
} layers = layers.where((x) => !x.isDeleted).toList();
layers = layers.where((x) => !x.isDeleted).toList(); setState(() {});
setState(() {}); },
}, ),
), ),
), ],
], ),
), ),
), ),
), ),
), Positioned(
Positioned( top: 10,
top: 10, left: 5,
left: 5, right: 0,
right: 0,
child: SafeArea(
child: Row(
children: actionsAtTheTop,
),
),
),
Positioned(
right: 6,
top: 100,
child: Container(
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.symmetric(vertical: 16),
child: SafeArea( child: SafeArea(
child: Column( child: Row(
mainAxisAlignment: MainAxisAlignment.center, children: actionsAtTheTop,
children: actionsAtTheRight,
), ),
), ),
), ),
), Positioned(
], right: 6,
top: 100,
child: Container(
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.symmetric(vertical: 16),
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: actionsAtTheRight,
),
),
),
),
],
),
), ),
); );
} }

View file

@ -221,7 +221,8 @@ class _UserListItem extends State<GroupListItem> {
: Row( : Row(
children: [ children: [
LastMessageTime( LastMessageTime(
dateTime: widget.group.lastMessageExchange), dateTime: widget.group.lastMessageExchange,
),
FlameCounterWidget( FlameCounterWidget(
groupId: widget.group.groupId, groupId: widget.group.groupId,
prefix: true, prefix: true,

View file

@ -36,8 +36,8 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
_flameCounterSub = stream.listen((counter) { _flameCounterSub = stream.listen((counter) {
if (mounted) { if (mounted) {
setState(() { setState(() {
_flameCounter = counter - // in the watchFlameCounter a one is added, so remove this here
1; // in the watchFlameCounter a one is added, so remove this here _flameCounter = counter - 1;
}); });
} }
}); });
@ -84,7 +84,7 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_directChat == null || if (_directChat == null ||
_directChat!.maxFlameCounter == 0 || _directChat!.maxFlameCounter <= 2 ||
_flameCounter >= _directChat!.maxFlameCounter || _flameCounter >= _directChat!.maxFlameCounter ||
_directChat!.maxFlameCounterFrom! _directChat!.maxFlameCounterFrom!
.isBefore(DateTime.now().subtract(const Duration(days: 4)))) { .isBefore(DateTime.now().subtract(const Duration(days: 4)))) {

View file

@ -58,7 +58,10 @@ class _MediaViewSizingState extends State<MediaViewSizing> {
if (widget.bottomNavigation != null) { if (widget.bottomNavigation != null) {
if (needToDownSizeImage) { if (needToDownSizeImage) {
imageChild = Expanded(child: imageChild); imageChild = Expanded(child: imageChild);
bottomNavigation = widget.bottomNavigation!; bottomNavigation = SizedBox(
height: widget.requiredHeight,
child: widget.bottomNavigation,
);
} else { } else {
bottomNavigation = Expanded(child: widget.bottomNavigation!); bottomNavigation = Expanded(child: widget.bottomNavigation!);
} }

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.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/utils/storage.dart';
@ -14,24 +15,6 @@ class DataAndStorageView extends StatefulWidget {
} }
class _DataAndStorageViewState extends State<DataAndStorageView> { class _DataAndStorageViewState extends State<DataAndStorageView> {
Map<String, List<String>> autoDownloadOptions = defaultAutoDownloadOptions;
bool storeMediaFilesInGallery = true;
@override
void initState() {
super.initState();
}
Future<void> initAsync() async {
final user = await getUser();
if (user == null) return;
setState(() {
autoDownloadOptions =
user.autoDownloadOptions ?? defaultAutoDownloadOptions;
storeMediaFilesInGallery = user.storeMediaFilesInGallery;
});
}
Future<void> showAutoDownloadOptions( Future<void> showAutoDownloadOptions(
BuildContext context, BuildContext context,
ConnectivityResult connectionMode, ConnectivityResult connectionMode,
@ -41,10 +24,11 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AutoDownloadOptionsDialog( return AutoDownloadOptionsDialog(
autoDownloadOptions: autoDownloadOptions, autoDownloadOptions:
gUser.autoDownloadOptions ?? defaultAutoDownloadOptions,
connectionMode: connectionMode, connectionMode: connectionMode,
onUpdate: () async { onUpdate: () async {
await initAsync(); setState(() {});
}, },
); );
}, },
@ -53,14 +37,16 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
Future<void> toggleStoreInGallery() async { Future<void> toggleStoreInGallery() async {
await updateUserdata((u) { await updateUserdata((u) {
u.storeMediaFilesInGallery = !storeMediaFilesInGallery; u.storeMediaFilesInGallery = !u.storeMediaFilesInGallery;
return u; return u;
}); });
await initAsync(); setState(() {});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final autoDownloadOptions =
gUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.settingsStorageData), title: Text(context.lang.settingsStorageData),
@ -72,7 +58,7 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
subtitle: Text(context.lang.settingsStorageDataStoreInGSubtitle), subtitle: Text(context.lang.settingsStorageDataStoreInGSubtitle),
onTap: toggleStoreInGallery, onTap: toggleStoreInGallery,
trailing: Switch( trailing: Switch(
value: storeMediaFilesInGallery, value: gUser.storeMediaFilesInGallery,
onChanged: (a) => toggleStoreInGallery(), onChanged: (a) => toggleStoreInGallery(),
), ),
), ),
@ -157,6 +143,14 @@ class _AutoDownloadOptionsDialogState extends State<AutoDownloadOptionsDialog> {
await _updateAutoDownloadSetting(DownloadMediaTypes.video, value); await _updateAutoDownloadSetting(DownloadMediaTypes.video, value);
}, },
), ),
CheckboxListTile(
title: const Text('Audio'),
value: autoDownloadOptions[widget.connectionMode.name]!
.contains(DownloadMediaTypes.audio.name),
onChanged: (bool? value) async {
await _updateAutoDownloadSetting(DownloadMediaTypes.audio, value);
},
),
], ],
), ),
actions: [ actions: [

View file

@ -28,7 +28,7 @@ class _DiagnosticsViewState extends State<DiagnosticsView> {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Diagnostics')), appBar: AppBar(title: const Text('Diagnostics')),
body: FutureBuilder<String>( body: FutureBuilder<String>(
future: loadLogFile(), future: readLast1000Lines(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());

View file

@ -1,6 +1,7 @@
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:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:twonly/globals.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/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
@ -11,8 +12,22 @@ import 'package:twonly/src/views/settings/help/diagnostics.view.dart';
import 'package:twonly/src/views/settings/help/faq.view.dart'; import 'package:twonly/src/views/settings/help/faq.view.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class HelpView extends StatelessWidget { class HelpView extends StatefulWidget {
const HelpView({super.key}); const HelpView({super.key});
@override
State<HelpView> createState() => _HelpViewState();
}
class _HelpViewState extends State<HelpView> {
Future<void> toggleAllowErrorTrackingViaSentry() async {
await updateUserdata((u) {
u.allowErrorTrackingViaSentry = !u.allowErrorTrackingViaSentry;
return u;
});
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -49,7 +64,10 @@ class HelpView extends StatelessWidget {
), ),
ListTile( ListTile(
title: Text(context.lang.settingsResetTutorials), title: Text(context.lang.settingsResetTutorials),
subtitle: Text(context.lang.settingsResetTutorialsDesc), subtitle: Text(
context.lang.settingsResetTutorialsDesc,
style: const TextStyle(fontSize: 12),
),
onTap: () async { onTap: () async {
await updateUserdata((user) { await updateUserdata((user) {
user.tutorialDisplayed = []; user.tutorialDisplayed = [];
@ -65,6 +83,32 @@ class HelpView extends StatelessWidget {
}, },
), ),
const Divider(), const Divider(),
ListTile(
title: Text(context.lang.allowErrorTracking),
subtitle: Text(
context.lang.allowErrorTrackingSubtitle,
style: const TextStyle(fontSize: 10),
),
onTap: toggleAllowErrorTrackingViaSentry,
trailing: Switch(
value: gUser.allowErrorTrackingViaSentry,
onChanged: (a) => toggleAllowErrorTrackingViaSentry(),
),
),
ListTile(
title: Text(context.lang.settingsHelpDiagnostics),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const DiagnosticsView();
},
),
);
},
),
const Divider(),
FutureBuilder( FutureBuilder(
future: PackageInfo.fromPlatform(), future: PackageInfo.fromPlatform(),
builder: (context, snap) { builder: (context, snap) {
@ -97,19 +141,6 @@ class HelpView extends StatelessWidget {
); );
}, },
), ),
ListTile(
title: Text(context.lang.settingsHelpDiagnostics),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const DiagnosticsView();
},
),
);
},
),
ListTile( ListTile(
title: const Text('Changelog'), title: const Text('Changelog'),
onTap: () async { onTap: () async {

View file

@ -44,7 +44,8 @@ class _DatabaseMigrationViewState extends State<DatabaseMigrationView> {
Uint8List? avatarSvg; Uint8List? avatarSvg;
if (oldContact.avatarSvg != null) { if (oldContact.avatarSvg != null) {
avatarSvg = Uint8List.fromList( avatarSvg = Uint8List.fromList(
gzip.encode(utf8.encode(oldContact.avatarSvg!))); gzip.encode(utf8.encode(oldContact.avatarSvg!)),
);
} }
await twonlyDB.contactsDao.insertContact( await twonlyDB.contactsDao.insertContact(
ContactsCompanion( ContactsCompanion(

View file

@ -976,6 +976,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
jni:
dependency: transitive
description:
name: jni
sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1
url: "https://pub.dev"
source: hosted
version: "0.14.2"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -1160,6 +1168,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.1" version: "0.3.1"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "64e35e1e2e79da4e83f2ace3bf4e5437cef523f46c7db2eba9a1419c49573790"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
octo_image: octo_image:
dependency: transitive dependency: transitive
description: description:
@ -1440,6 +1456,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.8" version: "0.3.8"
sentry:
dependency: transitive
description:
name: sentry
sha256: "10a0bc25f5f21468e3beeae44e561825aaa02cdc6829438e73b9b64658ff88d9"
url: "https://pub.dev"
source: hosted
version: "9.8.0"
sentry_flutter:
dependency: "direct main"
description:
name: sentry_flutter
sha256: aafbf41c63c98a30b17bdbf3313424d5102db62b08735c44bff810f277e786a5
url: "https://pub.dev"
source: hosted
version: "9.8.0"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none' publish_to: 'none'
version: 0.0.66+66 version: 0.0.67+67
environment: environment:
sdk: ^3.6.0 sdk: ^3.6.0
@ -69,6 +69,7 @@ dependencies:
restart_app: ^1.3.2 restart_app: ^1.3.2
screenshot: ^3.0.0 screenshot: ^3.0.0
scrollable_positioned_list: ^0.3.8 scrollable_positioned_list: ^0.3.8
sentry_flutter: ^9.8.0
share_plus: ^12.0.0 share_plus: ^12.0.0
tutorial_coach_mark: ^1.3.0 tutorial_coach_mark: ^1.3.0
url_launcher: ^6.3.1 url_launcher: ^6.3.1
@ -76,25 +77,22 @@ dependencies:
web_socket_channel: ^3.0.1 web_socket_channel: ^3.0.1
dependency_overrides: dependency_overrides:
flutter_secure_storage_darwin:
git:
url: https://github.com/juliansteenbakker/flutter_secure_storage.git
ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop
path: flutter_secure_storage_darwin/
flutter_android_volume_keydown:
git:
url: https://github.com/yenchieh/flutter_android_volume_keydown.git
branch: fix/lStar-not-found-error
# hardcoding the mirror mode of the VideCapture to MIRROR_MODE_ON_FRONT_ONLY
camera_android_camerax: camera_android_camerax:
# path: ../flutter-packages/packages/camera/camera_android_camerax # path: ../flutter-packages/packages/camera/camera_android_camerax
git: git:
url: https://github.com/otsmr/flutter-packages.git url: https://github.com/otsmr/flutter-packages.git
path: packages/camera/camera_android_camerax path: packages/camera/camera_android_camerax
ref: aef58af205a5f3ce6588a5c59bb2e734aab943f0 ref: aef58af205a5f3ce6588a5c59bb2e734aab943f0
flutter_android_volume_keydown:
git:
url: https://github.com/yenchieh/flutter_android_volume_keydown.git
branch: fix/lStar-not-found-error
flutter_secure_storage_darwin:
git:
url: https://github.com/juliansteenbakker/flutter_secure_storage.git
ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop
path: flutter_secure_storage_darwin/
# hardcoding the mirror mode of the VideCapture to MIRROR_MODE_ON_FRONT_ONLY
dev_dependencies: dev_dependencies:
build_runner: ^2.4.15 build_runner: ^2.4.15