Merge pull request #325 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled

Multiple bug fixes
This commit is contained in:
Tobi 2025-11-30 12:54:52 +01:00 committed by GitHub
commit b306165681
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 148 additions and 94 deletions

View file

@ -4,6 +4,14 @@
This repository contains the complete source code of the [twonly](https://twonly.eu) apps.
<a href="https://testflight.apple.com/join/U9B3v2rk" >
<img alt="Get it on Testflight button" src="https://twonly.eu/assets/buttons/get-it-on-testflight.png"
width="100px" />
</a>
<a href="https://releases.twonly.eu/fdroid/repo/">
<img alt="Get it on F-Droid button" src="https://twonly.eu/assets/buttons/get-it-on-f-droid.webp" width="100px" />
</a>
## Features
- Offer a Snapchat™ like experience
@ -54,11 +62,11 @@ run-as eu.twonly.testing ls /data/user/0/eu.twonly.testing/
## Signing Keys
When you download the app **via GitHub** you can verify the signing keys using for example the [AppVerifyer](https://github.com/soupslurpr/AppVerifier) and the following SHA-256 fingerprint of the signing certificate.
When you download the app **via GitHub or F-Droid** you can verify the signing keys using for example the [AppVerifyer](https://github.com/soupslurpr/AppVerifier) and the following SHA-256 fingerprint of the signing certificate.
eu.twonly
E3:C4:4D:56:8C:67:F9:32:AC:8C:33:90:99:8A:B9:5E:E8:FF:2D:7A:07:3C:24:E3:66:77:93:E6:EA:CD:77:0A
## License
This project is licensed under the [GNU AGPL 3.0](LICENSE) license.
This project is licensed under the [GNU AGPL 3.0](LICENSE) license.

View file

@ -238,7 +238,7 @@ func getPushNotificationText(pushNotification: PushNotification) -> (String, Str
.text: "sent a message{inGroup}.",
.twonly: "sent a twonly{inGroup}.",
.video: "sent a video{inGroup}.",
.image: "sent a image{inGroup}.",
.image: "sent an image{inGroup}.",
.audio: "sent a voice message{inGroup}.",
.contactRequest: "wants to connect with you.",
.acceptRequest: "is now connected with you.",

View file

@ -287,7 +287,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
..where((t) => t.groupId.equals(groupId)))
.getSingle();
final totalMediaCounter = group.totalMediaCounter + (received ? 0 : 1);
final totalMediaCounter = group.totalMediaCounter + 1;
var flameCounter = group.flameCounter;
var maxFlameCounter = group.maxFlameCounter;
var maxFlameCounterFrom = group.maxFlameCounterFrom;

View file

@ -115,7 +115,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
milliseconds: group.deleteMessagesAfterMilliseconds,
),
);
final affected = await (delete(messages)
await (delete(messages)
..where(
(m) =>
m.groupId.equals(group.groupId) &
@ -127,7 +127,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
m.createdAt.isSmallerThanValue(deletionTime))),
))
.go();
Log.info('Deleted $affected messages.');
}
}

View file

@ -62,7 +62,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
return await (select(receipts)..where((t) => t.rowId.equals(id)))
.getSingle();
} catch (e) {
Log.error(e);
// ignore error, receipts is already in the database...
return null;
}
}

View file

@ -562,7 +562,7 @@
"notificationText": "sent a message{inGroup}.",
"notificationTwonly": "sent a twonly{inGroup}.",
"notificationVideo": "sent a video{inGroup}.",
"notificationImage": "sent a image{inGroup}.",
"notificationImage": "sent an image{inGroup}.",
"notificationAudio": "sent a voice message{inGroup}.",
"notificationAddedToGroup": "has added you to \"{groupname}\"",
"notificationContactRequest": "wants to connect with you.",

View file

@ -2477,7 +2477,7 @@ abstract class AppLocalizations {
/// No description provided for @notificationImage.
///
/// In en, this message translates to:
/// **'sent a image{inGroup}.'**
/// **'sent an image{inGroup}.'**
String notificationImage(Object inGroup);
/// No description provided for @notificationAudio.

View file

@ -1345,7 +1345,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String notificationImage(Object inGroup) {
return 'sent a image$inGroup.';
return 'sent an image$inGroup.';
}
@override

View file

@ -185,7 +185,7 @@ class ApiService {
}
Future<void> _onError(dynamic e) async {
Log.error('websocket error: $e');
Log.warn('websocket error: $e');
await onClosed();
}
@ -206,7 +206,7 @@ class ApiService {
Future<server.ServerToClient?> _waitForResponse(Int64 seq) async {
final startTime = DateTime.now();
const timeout = Duration(seconds: 20);
const timeout = Duration(seconds: 60);
while (true) {
if (messagesV0[seq] != null) {
@ -215,7 +215,7 @@ class ApiService {
return tmp;
}
if (DateTime.now().difference(startTime) > timeout) {
Log.error('Timeout for message $seq');
Log.warn('Timeout for message $seq');
return null;
}
await Future.delayed(const Duration(milliseconds: 10));
@ -283,10 +283,6 @@ class ApiService {
request.v0.seq = seq;
final requestBytes = request.writeToBuffer();
Log.info(
'Sending ${requestBytes.length} bytes to the server via WebSocket.',
);
if (ensureRetransmission) {
await addToRetransmissionBuffer(seq, requestBytes);
}
@ -421,7 +417,7 @@ class ApiService {
final result = await sendRequestSync(req, authenticated: false);
if (result.isError) {
Log.error('could not request auth challenge', result);
Log.warn('could not request auth challenge', result);
return;
}

View file

@ -100,9 +100,11 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
failed = false;
} else {
failed = true;
Log.error(
'Got invalid response status code: ${update.responseStatusCode}',
);
if (update.responseStatusCode != null) {
Log.error(
'Got invalid response status code: ${update.responseStatusCode}',
);
}
}
} else {
Log.info('Got ${update.status} for $mediaId');
@ -110,7 +112,6 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
}
if (failed) {
Log.error('Background media upload failed: ${update.status}');
await requestMediaReupload(mediaId);
} else {
await handleEncryptedFile(mediaId);

View file

@ -44,7 +44,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
if (receipt == null) {
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
if (receipt == null) {
Log.error('Receipt not found.');
Log.warn('Receipt not found.');
return null;
}
}

View file

@ -134,7 +134,6 @@ Future<void> handleClient2ClientMessage(int fromUserId, Uint8List body) async {
..receiptId = receiptId
..type = Message_Type.PLAINTEXT_CONTENT
..plaintextContent = responsePlaintextContent;
Log.error('Sending decryption error');
} else {
response = Message()..type = Message_Type.SENDER_DELIVERY_RECEIPT;
}

View file

@ -22,6 +22,13 @@ Future<void> initFCMAfterAuthenticated() async {
final storedToken = await storage.read(key: SecureStorageKeys.googleFcm);
try {
if (Platform.isIOS) {
final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
if (apnsToken == null) {
Log.error('Error getting apnsToken');
return;
}
}
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken == null) {
Log.error('Error getting fcmToken');

View file

@ -35,11 +35,7 @@ class MediaFileService {
final service = await MediaFileService.fromMediaId(mediaId);
if (service == null) {
Log.error(
'Purging media file, as it is not in the database $mediaId.',
);
} else {
if (service != null) {
if (service.mediaFile.isDraftMedia) {
delete = false;
}

View file

@ -112,7 +112,7 @@ Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
return (EncryptedContent.fromBuffer(plaintext), null);
} on InvalidKeyIdException catch (e) {
Log.error(e);
Log.warn(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN);
} catch (e) {
Log.error(e);

View file

@ -50,8 +50,7 @@ Future<void> requestNewPrekeysForContact(int contactId) async {
.toList();
await twonlyDB.signalDao.insertPreKeys(preKeys);
} else {
// 104400
Log.error('[PREKEY] Could not load new pre keys for user $contactId');
Log.warn('[PREKEY] Could not load new pre keys for user $contactId');
}
});
}
@ -85,7 +84,7 @@ Future<void> requestNewSignedPreKeyForContact(int contactId) async {
),
);
} else {
Log.error('could not load new signed pre key for user $contactId');
Log.warn('could not load new signed pre key for user $contactId');
}
});
}

View file

@ -203,9 +203,6 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) {
Log.error(
'twonly Backup upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}',
);
await updateUserdata((user) {
if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;

View file

@ -18,10 +18,24 @@ void initLogger() {
);
}
});
cleanLogFile();
}
class Log {
static void error(Object? message, [Object? error, StackTrace? stackTrace]) {
static String filterLogMessage(String msg) {
if (msg.contains('SqliteException')) {
// Do not log data which would be inserted into the DB.
return msg.substring(0, msg.indexOf('parameters: '));
}
return msg;
}
static void error(
Object? messageInput, [
Object? error,
StackTrace? stackTrace,
]) {
final message = filterLogMessage('$messageInput');
if (globalAllowErrorTrackingViaSentry) {
try {
throw Exception(message);
@ -32,11 +46,21 @@ class Log {
Logger(_getCallerSourceCodeFilename()).shout(message, error, stackTrace);
}
static void warn(Object? message, [Object? error, StackTrace? stackTrace]) {
static void warn(
Object? messageInput, [
Object? error,
StackTrace? stackTrace,
]) {
final message = filterLogMessage('$messageInput');
Logger(_getCallerSourceCodeFilename()).warning(message, error, stackTrace);
}
static void info(Object? message, [Object? error, StackTrace? stackTrace]) {
static void info(
Object? messageInput, [
Object? error,
StackTrace? stackTrace,
]) {
final message = filterLogMessage('$messageInput');
Logger(_getCallerSourceCodeFilename()).fine(message, error, stackTrace);
}
}
@ -77,6 +101,23 @@ Future<void> _writeLogToFile(LogRecord record) async {
});
}
Future<void> cleanLogFile() async {
final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log');
if (logFile.existsSync()) {
final lines = await logFile.readAsLines();
if (lines.length <= 5000) return;
final removeCount = lines.length - 5000;
final remaining = lines.sublist(removeCount);
final sink = logFile.openWrite()..writeAll(remaining, '\n');
await sink.close();
}
}
Future<bool> deleteLogFile() async {
final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log');

View file

@ -6,7 +6,6 @@ import 'dart:math';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/camera/camera_preview_controller_view.dart';
class CameraZoomButtons extends StatefulWidget {
@ -51,7 +50,6 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
Future<void> initAsync() async {
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
Log.info('Found ${gCameras.length} cameras for zoom.');
var index =
gCameras.indexWhere((t) => t.lensType == CameraLensType.ultraWide);

View file

@ -90,11 +90,15 @@ class _TextViewState extends State<TextLayer> {
final bottom = MediaQuery.of(context).viewInsets.bottom +
MediaQuery.of(context).viewPadding.bottom;
// On Android it is possible to close the keyboard without `onEditingComplete` is triggered.
if (maxBottomInset > bottom) {
maxBottomInset = 0;
if (widget.layerData.isEditing) {
widget.layerData.isEditing = false;
onEditionComplete();
// prevent that the text element will be disappearing in case the keyboard just switches for example to the emoji page
if (bottom < 20) {
maxBottomInset = 0;
if (widget.layerData.isEditing) {
widget.layerData.isEditing = false;
onEditionComplete();
}
}
} else {
maxBottomInset = bottom;

View file

@ -6,7 +6,6 @@ import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hashlib/random.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
@ -431,9 +430,17 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
// In case the image was already stored, then rename the stored image.
if (mediaService.storedPath.existsSync()) {
final newPath = mediaService.storedPath.absolute.path
.replaceFirst(media.mediaId, uuid.v7());
mediaService.storedPath.renameSync(newPath);
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
type: Value(mediaService.mediaFile.type),
createdAt: Value(DateTime.now()),
stored: const Value(true),
),
);
if (mediaFile != null) {
mediaService.storedPath
.renameSync(MediaFileService(mediaFile).storedPath.path);
}
}
return true;
}

View file

@ -260,6 +260,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
});
final items = await MemoryItem.convertFromMessages(storedMediaFiles);
if (!mounted) return;
galleryItems = items.values.toList();
setState(() {});
}

View file

@ -76,11 +76,7 @@ class _MessageInputState extends State<MessageInput> {
}
void _initializeControllers() {
recorderController = RecorderController()
..androidEncoder = AndroidEncoder.aac
..androidOutputFormat = AndroidOutputFormat.mpeg4
..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC
..sampleRate = 44100;
recorderController = RecorderController();
}
void _handleTextFocusChange() {

View file

@ -172,12 +172,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
showSendTextMessageInput = false;
});
// if (Platform.isAndroid) {
// await flutterLocalNotificationsPlugin
// .cancel(allMediaFiles.first.contactId);
// } else {
await flutterLocalNotificationsPlugin.cancelAll();
// }
unawaited(flutterLocalNotificationsPlugin.cancelAll());
final stream =
twonlyDB.mediaFilesDao.watchMedia(allMediaFiles.first.mediaId!);
@ -261,6 +256,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
return nextMediaOrExit();
}
var timerRequired = false;
if (currentMediaLocal.mediaFile.type == MediaType.video) {
videoController = VideoPlayerController.file(currentMediaLocal.tempPath);
await videoController?.setLooping(
@ -292,12 +289,17 @@ class _MediaViewerViewState extends State<MediaViewerView> {
currentMediaLocal.mediaFile.displayLimitInMilliseconds!,
),
);
timerRequired = true;
}
}
if (mounted) {
setState(() {
currentMedia = currentMediaLocal;
});
if (timerRequired) {
startTimer();
}
}
setState(() {
currentMedia = currentMediaLocal;
});
}
void startTimer() {
@ -310,14 +312,16 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}
});
progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) {
if (currentMedia!.mediaFile.displayLimitInMilliseconds == null ||
final mediaFile = currentMedia?.mediaFile;
if (mediaFile == null) return;
if (mediaFile.displayLimitInMilliseconds == null ||
canBeSeenUntil == null) {
return;
}
final difference = canBeSeenUntil!.difference(DateTime.now());
// Calculate the progress as a value between 0.0 and 1.0
progress = difference.inMilliseconds /
(currentMedia!.mediaFile.displayLimitInMilliseconds!);
progress =
difference.inMilliseconds / (mediaFile.displayLimitInMilliseconds!);
setState(() {});
});
}

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
class FlameCounterWidget extends StatefulWidget {
@ -38,18 +39,20 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
Future<void> initAsync() async {
var groupId = widget.groupId;
late Group? group;
if (widget.groupId == null && widget.contactId != null) {
final group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!);
group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!);
groupId = group?.groupId;
} else if (groupId != null) {
// do not display the flame counter for groups
final group = await twonlyDB.groupsDao.getGroup(groupId);
group = await twonlyDB.groupsDao.getGroup(groupId);
if (!(group?.isDirectChat ?? false)) {
return;
}
}
if (groupId != null) {
isBestFriend = gUser.myBestFriendGroupId == groupId;
if (groupId != null && group != null) {
isBestFriend =
gUser.myBestFriendGroupId == groupId && group.alsoBestFriend;
final stream = twonlyDB.groupsDao.watchFlameCounter(groupId);
flameCounterSub = stream.listen((counter) {
if (mounted) {

View file

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

View file

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -61,10 +61,10 @@ packages:
dependency: "direct main"
description:
name: audio_waveforms
sha256: "658fef41bbab299184b65ba2fd749e8ec658c1f7d54a21f7cf97fa96b173b4ce"
sha256: "3a34bdd15dd63a6d1501218449048b28ebe8e1f795bf00ec310acd7b70648f07"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "2.0.0"
avatar_maker:
dependency: "direct main"
description:
@ -77,10 +77,10 @@ packages:
dependency: "direct main"
description:
name: background_downloader
sha256: a913b37cc47a656a225e9562b69576000d516f705482f392e2663500e6ff6032
sha256: a3b340e42bc45598918944e378dc6a05877e587fcd0e1b8d2ea26339de87bdf9
url: "https://pub.dev"
source: hosted
version: "9.3.0"
version: "9.4.0"
boolean_selector:
dependency: transitive
description:
@ -397,11 +397,12 @@ packages:
emoji_picker_flutter:
dependency: "direct main"
description:
name: emoji_picker_flutter
sha256: "9a44c102079891ea5877f78c70f2e3c6e9df7b7fe0a01757d31f1046eeaa016d"
url: "https://pub.dev"
source: hosted
version: "4.3.0"
path: "."
ref: HEAD
resolved-ref: c5bffd3414c1e640389b41165b831df7df1cf517
url: "https://github.com/otsmr/emoji_picker_flutter.git"
source: git
version: "4.4.0"
fake_async:
dependency: transitive
description:
@ -1741,14 +1742,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
url_launcher:
dependency: "direct main"
description:
@ -1865,10 +1858,10 @@ packages:
dependency: "direct main"
description:
name: video_player
sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a"
sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
version: "2.10.1"
video_player_android:
dependency: transitive
description:

View file

@ -3,16 +3,16 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none'
version: 0.0.71+71
version: 0.0.72+72
environment:
sdk: ^3.6.0
dependencies:
archive: ^4.0.7
audio_waveforms: ^1.3.0
audio_waveforms: ^2.0.0
avatar_maker: ^0.4.0
background_downloader: ^9.2.2
background_downloader: ^9.4.0
cached_network_image: ^3.4.1
camera: ^0.11.2
collection: ^1.18.0
@ -74,9 +74,9 @@ dependencies:
sentry_flutter: ^9.8.0
share_plus: ^12.0.0
tutorial_coach_mark: ^1.3.0
url_launcher: ^6.3.1
url_launcher: ^6.3.2
vector_graphics: ^1.1.19
video_player: ^2.9.5
video_player: ^2.10.1
web_socket_channel: ^3.0.1
dependency_overrides:
@ -86,6 +86,11 @@ dependency_overrides:
url: https://github.com/otsmr/flutter-packages.git
path: packages/camera/camera_android_camerax
ref: aef58af205a5f3ce6588a5c59bb2e734aab943f0
emoji_picker_flutter:
# Fixes the issue with recent emojis (solved by https://github.com/Fintasys/emoji_picker_flutter/pull/238)
# Using override until this gets merged.
git:
url: https://github.com/otsmr/emoji_picker_flutter.git
flutter_android_volume_keydown:
git:
url: https://github.com/yenchieh/flutter_android_volume_keydown.git