finish onboarding and some more stuff

This commit is contained in:
otsmr 2025-02-09 18:02:52 +01:00
parent c3f429a90a
commit 5f2a9890ba
34 changed files with 559 additions and 114 deletions

View file

@ -10,6 +10,7 @@ Don't be lonely, get twonly! Send pictures to a friend in real time and be sure
## Maybe
- Send a picture first to only one person -> Kamera button
- Response to a image
- MediaView:
- Bei weiteren geladenen Bildern -> Direkt anzeigen ohne pop

View file

@ -30,12 +30,33 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:foregroundServiceType="dataSync|remoteMessaging"
android:exported="false" />
<meta-data
android:name="eu.twonly.service.TWONLY_LOGO"
android:resource="@drawable/ic_launcher_foreground" />
</application>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

5
lib/globals.dart Normal file
View file

@ -0,0 +1,5 @@
import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart';
late DbProvider dbProvider;
late ApiProvider apiProvider;

View file

@ -1,5 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart';
@ -13,9 +15,6 @@ import 'package:twonly/src/services/notification_service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'src/app.dart';
late DbProvider dbProvider;
late ApiProvider apiProvider;
void main() async {
final settingsController = SettingsChangeProvider();
@ -39,23 +38,11 @@ void main() async {
await initMediaStorage();
dbProvider = DbProvider();
// Database is just a file, so this will not block the loading of the app much
await dbProvider.ready;
var apiUrl = "ws://api.twonly.eu/api/client";
var backupApiUrl = "ws://api2.twonly.eu/api/client";
// if (!kReleaseMode) {
// Overwrite the domain in your local network so you can test the app locally
apiUrl = "ws://10.99.0.6:3030/api/client";
// }
apiProvider = ApiProvider();
apiProvider = ApiProvider(apiUrl: apiUrl, backupApiUrl: backupApiUrl);
// Workmanager.executeTask((task, inputData) async {
// await _HomeState().manager();
// print('Background Services are Working!');//This is Working
// return true;
// });
FlutterForegroundTask.initCommunicationPort();
runApp(
MultiProvider(

View file

@ -1,9 +1,12 @@
import 'dart:io';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:provider/provider.dart';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart';
import 'package:twonly/src/providers/download_change_provider.dart';
import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/providers/settings_change_provider.dart';
import 'package:twonly/src/tasks/websocket_foreground_task.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/onboarding/onboarding_view.dart';
import 'package:twonly/src/views/home_view.dart';
@ -72,24 +75,127 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
// connect async to the backend api
apiProvider.connect();
FlutterForegroundTask.addTaskDataCallback(_onReceiveTaskData);
WidgetsBinding.instance.addPostFrameCallback((_) {
_requestPermissions();
_initService();
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
Future<void> _requestPermissions() async {
// Android 13+, you need to allow notification permission to display foreground service notification.
//
// iOS: If you need notification, ask for permission.
final NotificationPermission notificationPermission =
await FlutterForegroundTask.checkNotificationPermission();
if (notificationPermission != NotificationPermission.granted) {
await FlutterForegroundTask.requestNotificationPermission();
}
if (Platform.isAndroid) {
// Android 12+, there are restrictions on starting a foreground service.
//
// To restart the service on device reboot or unexpected problem, you need to allow below permission.
if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
// This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.
await FlutterForegroundTask.requestIgnoreBatteryOptimization();
}
// Use this utility only if you provide services that require long-term survival,
// such as exact alarm service, healthcare service, or Bluetooth communication.
//
// This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission.
// Using this permission may make app distribution difficult due to Google policy.
// if (!await FlutterForegroundTask.canScheduleExactAlarms) {
// When you call this function, will be gone to the settings page.
// So you need to explain to the user why set it.
// await FlutterForegroundTask.openAlarmsAndRemindersSettings();
// }
}
}
void _onReceiveTaskData(Object data) {
if (data is Map<String, dynamic>) {
final dynamic timestampMillis = data["timestampMillis"];
if (timestampMillis != null) {
final DateTime timestamp =
DateTime.fromMillisecondsSinceEpoch(timestampMillis, isUtc: true);
print('timestamp: ${timestamp.toString()}');
}
}
}
void _initService() {
FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'foreground_service',
channelName: 'Foreground Service Notification',
channelDescription:
'This notification appears when the foreground service is running.',
onlyAlertOnce: true,
),
iosNotificationOptions: const IOSNotificationOptions(
showNotification: false,
playSound: false,
),
foregroundTaskOptions: ForegroundTaskOptions(
eventAction: ForegroundTaskEventAction.repeat(5000),
autoRunOnBoot: true,
autoRunOnMyPackageReplaced: true,
allowWakeLock: true,
allowWifiLock: true,
),
);
}
Future<ServiceRequestResult> _startService() async {
if (await FlutterForegroundTask.isRunningService) {
return FlutterForegroundTask.restartService();
} else {
return FlutterForegroundTask.startService(
serviceId: 256,
notificationTitle: 'Foreground Service is running',
notificationText: 'Tap to return to the app',
notificationIcon:
NotificationIcon(metaDataName: "eu.twonly.service.TWONLY_LOGO"),
notificationInitialRoute: '/',
callback: startCallback,
);
}
}
Future _stopService() async {
await FlutterForegroundTask.stopService();
if (!apiProvider.isAuthenticated) {
apiProvider.connect();
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
print("STATE: $state");
if (state == AppLifecycleState.resumed) {
_stopService();
//apiProvider.connect();
} else if (state == AppLifecycleState.paused) {
apiProvider.close(() {
_startService();
});
}
}
@override
void dispose() {
print("STATE: dispose");
// apiProvider.close(() {});
WidgetsBinding.instance.removeObserver(this);
// disable globalCallbacks to the flutter tree
globalCallbackConnectionState = (a) {};
globalCallBackOnDownloadChange = (a, b) {};
globalCallBackOnContactChange = () {};
globalCallBackOnMessageChange = (a) {};
FlutterForegroundTask.removeTaskDataCallback(_onReceiveTaskData);
super.dispose();
}

View file

@ -39,13 +39,25 @@ class InitialsAvatar extends StatelessWidget {
double proSize = (fontSize == null) ? 40 : (fontSize! * 2);
return isPro
? ClipRRect(
borderRadius: BorderRadius.circular(12.0), //or 15.0
child: Container(
height: proSize,
width: proSize,
color: avatarColor,
child: Center(child: child),
? //or 15.0
Container(
constraints: BoxConstraints(
minHeight: 2 * (fontSize ?? 20),
minWidth: 2 * (fontSize ?? 20),
maxWidth: 2 * (fontSize ?? 20),
maxHeight: 2 * (fontSize ?? 20),
),
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(12.0),
child: Container(
height: proSize,
width: proSize,
//padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
color: avatarColor,
child: Center(child: child),
),
),
),
)
: CircleAvatar(

View file

@ -1,4 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:permission_handler/permission_handler.dart';
class PermissionHandlerView extends StatefulWidget {
@ -44,6 +47,28 @@ class PermissionHandlerViewState extends State<PermissionHandlerView> {
// if (statuses[Permission.camera]!.isDenied) {
// }
}
if (Platform.isAndroid) {
// Android 12+, there are restrictions on starting a foreground service.
//
// To restart the service on device reboot or unexpected problem, you need to allow below permission.
if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) {
// This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.
await FlutterForegroundTask.requestIgnoreBatteryOptimization();
}
// Use this utility only if you provide services that require long-term survival,
// such as exact alarm service, healthcare service, or Bluetooth communication.
//
// This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission.
// Using this permission may make app distribution difficult due to Google policy.
// if (!await FlutterForegroundTask.canScheduleExactAlarms) {
// When you call this function, will be gone to the settings page.
// So you need to explain to the user why set it.
// await FlutterForegroundTask.openAlarmsAndRemindersSettings();
// }
}
/*{Permission.camera: PermissionStatus.granted, Permission.storage: PermissionStatus.granted}*/
return statuses;
}

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/views/chats/chat_item_details_view.dart';
import 'package:twonly/src/views/contact/contact_verify_view.dart';
class UserContextMenu extends StatefulWidget {
@ -33,6 +34,17 @@ class _UserContextMenuState extends State<UserContextMenu> {
? FaIcon(FontAwesomeIcons.shieldHeart)
: const Icon(Icons.gpp_maybe_rounded), // Can be any widget
),
PieAction(
tooltip: const Text('Open chat'),
onSelect: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return ChatItemDetailsView(user: widget.user);
},
));
},
child: const FaIcon(FontAwesomeIcons.solidComments),
),
PieAction(
tooltip: const Text('Send image'),
onSelect: () {

View file

@ -1,6 +1,97 @@
{
"@@locale": "de",
"registerTitle": "Willkommen bei twonly",
"@registerTitle": {},
"registerSlogan": "Sende Bilder in Echtzeit an Freunde und sei dir sicher, dass nur ihr sie sehen könnt."
"registerTitle": "Willkommen bei twonly!",
"registerSlogan": "twonly, ein datenschutzfreundlicher Weg, um sich mit Freunden durch sicheren, spontanen Bildaustausch zu verbinden",
"onboardingWelcomeTitle": "Willkommen bei twonly!",
"onboardingWelcomeBody": "Erlebe eine datenschutzfreundliche Möglichkeit sich mit Freunden durch sicheren, spontanen Bildaustausch zu verbinden.",
"onboardingE2eTitle": "Ende-zu-Ende-Verschlüsselung",
"onboardingE2eBody": "Deine Privatsphäre ist uns wichtig! Nur aus diesem Grund wurde twonly entwickelt. Genieße durch die Ende-zu-Ende-Verschlüsselung die Gewissheit, dass nur du und deine Freunde die geteilten Momente sehen können.",
"onboardingFocusTitle": "Fokussiere dich auf das Teilen von Momenten",
"onboardingFocusBody": "Verabschiede dich von süchtig machenden Funktionen! Unsere App wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.",
"onboardingSendTwonliesTitle": "Twonlies senden",
"onboardingSendTwonliesBody": "Teile Momente sicher mit deinem Partner. twonly stellt sicher, dass nur dein Partner sie öffnen kann, sodass deine Momente mit deinem Partner eine two(o)nly Sache bleiben!",
"onboardingNotProductTitle": "Du bist nicht das Produkt!",
"onboardingNotProductBody": "Zahlst du für eine App nicht, dann werden deine Daten verkauft. Das entspricht nicht unseren Werten, deshalb haben wir uns entschieden, ein nachhaltiges Geschäftsmodell zu entwickeln, von dem alle profitieren. Du kannst deine Daten privat halten und wir können eine schöne App erstellen.",
"onboardingBuyOneGetTwoTitle": "Kaufe eins, bekomme zwei",
"onboardingBuyOneGetTwoBody": "Um eine werbefreie, datenschutzorientierte App zu schaffen, brauchen wir dich! Wir versuchen, dir den besten Preis anzubieten, damit du twonly für nur 0,99 € / monatlich oder 9,99 € / jährlich erhalten kannst und sogar eine zweite Lizenz kostenlos für deinen twonly-Partner bekommst!",
"onboardingGetStartedTitle": "Lass uns anfangen!",
"onboardingGetStartedBody": "Du kannst twonly 14 Tage lang kostenlos testen und dann entscheiden, ob es dir wert ist.",
"onboardingTryForFree": "Kostenlos testen",
"registerUsernameSlogan": "Bitte wähle einen Benutzernamen, damit dich andere finden können!",
"registerUsernameDecoration": "Benutzername",
"registerUsernameLimits": "Der Benutzername muss 4 bis 12 Zeichen lang sein und darf nur aus Buchstaben (a-z) und Zahlen (0-9) bestehen.",
"registerSubmitButton": "Jetzt registrieren!",
"newMessageTitle": "Neue Nachricht",
"chatsTitle": "Chats",
"shareImageTitle": "Teilen mit",
"shareImageBestFriends": "Beste Freunde",
"shareImagedEditorSendImage": "Senden",
"shareImagedEditorShareWith": "Teilen mit",
"shareImagedEditorSaveImage": "Speichern",
"shareImagedEditorSavedImage": "Gespeichert",
"shareImageAllUsers": "Alle Kontakte",
"shareImageAllTwonlyWarning": "Twonlies können nur an verifizierte Kontakte gesendet werden!",
"searchUsernameInput": "Benutzername",
"searchUsernameTitle": "Benutzernamen suchen",
"searchUsernameNotFound": "Benutzername nicht gefunden",
"searchUsernameNewFollowerTitle": "Folgeanfragen",
"searchUsernameQrCodeBtn": "QR-Code scannen",
"chatListViewSearchUserNameBtn": "Füge deinen ersten twonly-Kontakt hinzu!",
"chatListViewSendFirstTwonly": "Sende dein erstes twonly!",
"chatListDetailInput": "Nachricht eingeben",
"messageSendState_Received": "Empfangen",
"messageSendState_Opened": "Geöffnet",
"messageSendState_Send": "Senden",
"messageSendState_Sending": "Wird gesendet",
"messageSendState_TapToLoad": "Tippe zum Laden",
"messageSendState_Loading": "Herunterladen",
"imageEditorDrawOk": "Zeichnung machen",
"settingsTitle": "Einstellungen",
"settingsAccount": "Konto",
"settingsSubscription": "Abonnement",
"settingsAppearance": "Erscheinungsbild",
"settingsPrivacy": "Datenschutz",
"settingsPrivacyBlockUsers": "Benutzer blockieren",
"settingsPrivacyBlockUsersDesc": "Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.",
"settingsPrivacyBlockUsersCount": "{len} Kontakt(e)",
"settingsNotification": "Benachrichtigung",
"settingsHelp": "Hilfe",
"settingsHelpSupport": "Support-Center",
"settingsHelpVersion": "Version",
"settingsHelpLicenses": "Lizenzen",
"settingsHelpLegal": "Nutzungsbedingungen & Datenschutzrichtlinie",
"settingsAppearanceTheme": "Theme",
"settingsAccountDeleteAccount": "Konto löschen",
"settingsAccountDeleteModalTitle": "Bist du sicher?",
"settingsAccountDeleteModalBody": "Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.",
"contactVerifyNumberTitle": "Sicherheitsnummer verifizieren",
"contactVerifyNumberMarkAsVerified": "Als verifiziert markieren",
"contactVerifyNumberClearVerification": "Verifizierung aufheben",
"contactVerifyNumberLongDesc": "Um die Ende-zu-Ende-Verschlüsselung mit {username} zu verifizieren, vergleiche die Zahlen mit ihrem Gerät. Die Person kann auch deinen Code mit ihrem Gerät scannen.",
"undo": "Rückgängig",
"redo": "Wiederholen",
"next": "Weiter",
"close": "Schließen",
"cancel": "Abbrechen",
"ok": "Ok",
"switchFrontAndBackCamera": "Zwischen Front- und Rückkamera wechseln.",
"addTextItem": "Text",
"protectAsARealTwonly": "Als echtes twonly senden!",
"addDrawing": "Zeichnung",
"addEmoji": "Emoji",
"toogleFlashLight": "Taschenlampe umschalten",
"searchUsernameNotFoundLong": "\"{username}\" ist kein twonly-Benutzer. Bitte überprüfe den Benutzernamen und versuche es erneut.",
"errorUnknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut.",
"errorBadRequest": "Die Anfrage konnte vom Server aufgrund einer fehlerhaften Syntax nicht verstanden werden. Bitte überprüfe deine Eingabe und versuche es erneut.",
"errorTooManyRequests": "Du hast in kurzer Zeit zu viele Anfragen gestellt. Bitte warte einen Moment, bevor du es erneut versuchst.",
"errorInternalError": "Der Server ist derzeit nicht verfügbar. Bitte versuche es später erneut.",
"errorInvalidInvitationCode": "Der von dir angegebene Einladungscode ist ungültig. Bitte überprüfe den Code und versuche es erneut.",
"errorUsernameAlreadyTaken": "Der Benutzername, den du verwenden möchtest, ist bereits vergeben. Bitte wähle einen anderen Benutzernamen.",
"errorSignatureNotValid": "Die bereitgestellte Signatur ist nicht gültig. Bitte überprüfe deine Anmeldeinformationen und versuche es erneut.",
"errorUsernameNotFound": "Der eingegebene Benutzername existiert nicht. Bitte überprüfe die Schreibweise oder erstelle ein neues Konto.",
"errorUsernameNotValid": "Der von dir angegebene Benutzername entspricht nicht den erforderlichen Kriterien. Bitte wähle einen gültigen Benutzernamen.",
"errorInvalidPublicKey": "Der von dir angegebene öffentliche Schlüssel ist ungültig. Bitte überprüfe den Schlüssel und versuche es erneut.",
"errorSessionAlreadyAuthenticated": "Du bist bereits angemeldet. Bitte melde dich ab, wenn du dich mit einem anderen Konto anmelden möchtest.",
"errorSessionNotAuthenticated": "Deine Sitzung ist nicht authentifiziert. Bitte melde dich an, um fortzufahren.",
"errorOnlyOneSessionAllowed": "Es ist nur eine aktive Sitzung pro Benutzer erlaubt. Bitte melde dich von anderen Geräten ab, um fortzufahren."
}

View file

@ -1,7 +1,22 @@
{
"@@locale": "en",
"registerTitle": "Welcome to twonly!",
"registerSlogan": "Send pictures to friends in real time and be sure you are the only people who can see them.",
"registerSlogan": "twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing",
"onboardingWelcomeTitle": "Welcome to twonly!",
"onboardingWelcomeBody": "Experience a privacy friendly way to connect with friends through secure, spontaneous image sharing.",
"onboardingE2eTitle": "End-to-End Encryption",
"onboardingE2eBody": "Your privacy matters! In fact, twonly was only created because there is no secure alternative. Enjoy peace of mind with end-to-end encryption that ensures only you and your friends can see your pictures.",
"onboardingFocusTitle": "Focus on sharing moments",
"onboardingFocusBody": "Say goodbye to addictive features! Our app was created for sharing moments, free from useless distractions or ads.",
"onboardingSendTwonliesTitle": "Send twonlies",
"onboardingSendTwonliesBody": "Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!",
"onboardingNotProductTitle": "You are not the product!",
"onboardingNotProductBody": "If you don't pay, your data is the product that is sold. So we decided to develop a sustainable business model where everyone wins. You can keep your data private and we can create a beautiful app.",
"onboardingBuyOneGetTwoTitle": "Buy one get two",
"onboardingBuyOneGetTwoBody": "To create a ad-free, privacy-focused app, we need you! We try to offer the best price for you, so you can get twonly for only 0,99€ / monthly or 9,99€ / yearly and even get a second license for free for your twonly partner!",
"onboardingGetStartedTitle": "Let's get started!",
"onboardingGetStartedBody": "You can test twonly free for 14 days and then decide if it is worth to you.",
"onboardingTryForFree": "Try for free",
"registerUsernameSlogan": "Please select a username so others can find you!",
"registerUsernameDecoration": "Username",
"registerUsernameLimits": "Username must be 4 to 12 characters long, consisting only of letters (a-z) and numbers (0-9).",
@ -55,6 +70,7 @@
"contactVerifyNumberLongDesc": "To verify the end-to-end encryption with {username}, compare the numbers with their device. The person can also scan your code with their device.",
"undo": "Undo",
"redo": "Redo",
"next": "Next",
"close": "Close",
"cancel": "Cancel",
"ok": "Ok",

View file

@ -3,7 +3,7 @@ import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/utils/misc.dart';

View file

@ -3,7 +3,7 @@ import 'dart:convert';
import 'package:cv/cv.dart';
import 'package:logging/logging.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/components/message_send_state_icon.dart';
import 'package:twonly/src/model/json/message.dart';

View file

@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart';
@ -130,12 +130,17 @@ Future uploadMediaFile(
if (uploadToken == null) return;
bool wasSend = await apiProvider.uploadData(uploadToken, encryptedMedia, 0);
Logger("api.dart").shout("UPDATE...");
// TODO: fragmented upload...
if (!await apiProvider.uploadData(uploadToken, encryptedMedia, 0)) {
if (!wasSend) {
Logger("api.dart").shout("error while uploading media");
return;
}
Logger("api.dart").shout("DOING UPDATE");
box.delete("retransmit-$messageId-media");
box.delete("retransmit-$messageId-uploadtoken");
await DbContacts.checkAndUpdateFlames(target.toInt());
@ -256,6 +261,7 @@ Future<Uint8List?> getDownloadedMedia(
box.delete(mediaToken.toString());
box.put("${mediaToken}_downloaded", "deleted");
box.delete("${mediaToken}_fromUserId");
box.delete("${mediaToken}_messageId");
return media;
}

View file

@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:logging/logging.dart';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart';
@ -54,6 +54,25 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
final box = await getMediaStorage();
String boxId = data.uploadToken.toString();
if (data.fin && data.data.isEmpty) {
// media file was deleted by the server. remove the media from device
int? messageId = box.get("${data.uploadToken}_messageId");
if (messageId != null) {
await DbMessages.deleteMessageById(messageId);
box.delete(boxId);
int? fromUserId = box.get("${data.uploadToken}_fromUserId");
if (fromUserId != null) {
globalCallBackOnMessageChange(fromUserId);
}
box.delete("${data.uploadToken}_fromUserId");
box.delete("${data.uploadToken}_downloaded");
globalCallBackOnDownloadChange(data.uploadToken, false);
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
}
Uint8List? buffered = box.get(boxId);
Uint8List downloadedBytes;
if (buffered != null) {
@ -164,6 +183,7 @@ Future<client.Response> handleNewMessage(
List<int> downloadToken = content.downloadToken;
Box box = await getMediaStorage();
box.put("${downloadToken}_fromUserId", fromUserId.toInt());
box.put("${downloadToken}_messageId", messageId);
tryDownloadMedia(downloadToken);
}
}

View file

@ -24,10 +24,11 @@ import 'package:web_socket_channel/web_socket_channel.dart';
/// It handles errors and does automatically tries to reconnect on
/// errors or network changes.
class ApiProvider {
final String apiUrl;
final String? backupApiUrl;
final String apiUrl = "ws://10.99.0.6:3030/api/client";
// ws://api.twonly.eu/api/client
final String? backupApiUrl = "ws://10.99.0.6:3030/api/client";
bool isAuthenticated = false;
ApiProvider({required this.apiUrl, required this.backupApiUrl});
ApiProvider();
final log = Logger("ApiProvider");
@ -62,6 +63,15 @@ class ApiProvider {
tryTransmitMessages();
}
Future close(Function callback) async {
if (_channel != null) {
await _channel!.sink.close();
callback();
return;
}
callback();
}
Future<bool> connect() async {
if (_channel != null && _channel!.closeCode != null) {
return true;
@ -94,7 +104,7 @@ class ApiProvider {
globalCallbackConnectionState(false);
_channel = null;
isAuthenticated = false;
tryToReconnect();
// tryToReconnect();
}
void _onError(dynamic e) {
@ -157,7 +167,9 @@ class ApiProvider {
}
Future sendResponse(ClientToServer response) async {
_channel!.sink.add(response.writeToBuffer());
if (_channel != null) {
_channel!.sink.add(response.writeToBuffer());
}
}
Future<Result> _sendRequestV0(ClientToServer request,
@ -184,6 +196,7 @@ class ApiProvider {
}
Future authenticate() async {
print("try authenticate $isAuthenticated");
if (isAuthenticated) return;
if (await SignalHelper.getSignalIdentity() == null) {
return;
@ -192,6 +205,7 @@ class ApiProvider {
var handshake = Handshake()..getchallenge = Handshake_GetChallenge();
var req = createClientToServerFromHandshake(handshake);
print("try authenticate send to server");
final result = await _sendRequestV0(req, authenticated: false);
if (result.isError) {
log.shout("Error auth", result);

View file

@ -144,10 +144,10 @@ String getPushNotificationText(String key, String userName) {
pushNotificationText = {
"newTextMessage": "%userName% hat die eine Nachricht gesendet.",
"newTwonly": "%userName% hat dir einen twonly gesendet.",
"newVideo": "%userName% hat die eine Video gesendet.",
"newImage": "%userName% hat die eine Bild gesendet.",
"newVideo": "%userName% hat dir eine Video gesendet.",
"newImage": "%userName% hat dir eine Bild gesendet.",
"contactRequest": "%userName% möchte sich mir dir vernetzen.",
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
};
} else {
pushNotificationText = {
@ -214,6 +214,6 @@ Future localPushNotificationNewMessage(
user.displayName,
msg,
notificationDetails,
// payload: 'test',
payload: message.kind.index.toString(),
);
}

View file

@ -1,6 +1,6 @@
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/identity_key_store_model.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';

View file

@ -1,5 +1,5 @@
import 'dart:typed_data';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/pre_key_model.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';

View file

@ -1,5 +1,5 @@
import 'dart:typed_data';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/sender_key_store_model.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';

View file

@ -1,5 +1,5 @@
import 'dart:typed_data';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/session_store_model.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';

View file

@ -0,0 +1,64 @@
// The callback function should always be a top-level or static function.
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart';
@pragma('vm:entry-point')
void startCallback() {
FlutterForegroundTask.setTaskHandler(WebsocketForegroundTask());
}
class WebsocketForegroundTask extends TaskHandler {
// Called when the task is started.
@override
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {
print('onStart(starter: ${starter.name})');
dbProvider = DbProvider();
await dbProvider.ready;
apiProvider = ApiProvider();
apiProvider.connect();
}
// Called based on the eventAction set in ForegroundTaskOptions.
@override
void onRepeatEvent(DateTime timestamp) {
// Send data to main isolate.
final Map<String, dynamic> data = {
"timestampMillis": timestamp.millisecondsSinceEpoch,
};
FlutterForegroundTask.sendDataToMain(data);
}
// Called when the task is destroyed.
@override
Future<void> onDestroy(DateTime timestamp) async {
await apiProvider.close(() {});
print('onDestroy');
}
// Called when data is sent using `FlutterForegroundTask.sendDataToTask`.
@override
void onReceiveData(Object data) {
print('onReceiveData: $data');
}
// Called when the notification button is pressed.
@override
void onNotificationButtonPressed(String id) {
print('onNotificationButtonPressed: $id');
}
// Called when the notification itself is pressed.
@override
void onNotificationPressed() {
print('onNotificationPressed');
}
// Called when the notification itself is dismissed.
@override
void onNotificationDismissed() {
print('onNotificationDismissed');
}
}

View file

@ -1,6 +1,6 @@
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/json/user_data.dart';
import 'package:twonly/src/utils/misc.dart';

View file

@ -265,7 +265,7 @@ class UserList extends StatelessWidget {
child: VerifiedShield(user),
),
Text(user.displayName),
if (flameCounter >= 0)
if (flameCounter >= 1)
FlameCounterWidget(
user,
flameCounter,

View file

@ -11,6 +11,7 @@ import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/download_change_provider.dart';
import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/services/notification_service.dart';
import 'package:twonly/src/views/chats/media_viewer_view.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact_view.dart';
@ -138,25 +139,17 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
}
Future _loadAsync({bool updateOpenStatus = false}) async {
if (_messages.isEmpty) {
_messages =
await DbMessages.getAllMessagesForUser(widget.user.userId.toInt());
} else {
int lastMessageId = _messages.first.messageId;
List<DbMessage> toAppend =
await DbMessages.getAllMessagesForUserWithHigherMessageId(
widget.user.userId.toInt(), lastMessageId);
_messages.insertAll(0, toAppend);
}
try {
if (context.mounted) {
setState(() {});
}
} catch (e) {
// state should be disposed
return;
}
// if (_messages.isEmpty || updateOpenStatus) {
_messages =
await DbMessages.getAllMessagesForUser(widget.user.userId.toInt());
// } else {
// will not update older message states like when they now downloaded...
// int lastMessageId = _messages.first.messageId;
// List<DbMessage> toAppend =
// await DbMessages.getAllMessagesForUserWithHigherMessageId(
// widget.user.userId.toInt(), lastMessageId);
// _messages.insertAll(0, toAppend);
// }
if (updateOpenStatus) {
_messages.where((x) => x.messageOpenedAt == null).forEach((message) {
@ -165,11 +158,20 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
if (!alreadyReportedOpened.contains(message.messageOtherId!)) {
userOpenedOtherMessage(
message.otherUserId, message.messageOtherId!);
flutterLocalNotificationsPlugin.cancel(message.messageId);
alreadyReportedOpened.add(message.messageOtherId!);
}
}
});
}
try {
if (context.mounted) {
setState(() {});
}
} catch (e) {
// state should be disposed
return;
}
}
Future _sendMessage() async {
@ -184,6 +186,7 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
final changeCounter = context.watch<MessagesChangeProvider>().changeCounter;
if (changeCounter.containsKey(widget.user.userId.toInt())) {
if (changeCounter[widget.user.userId.toInt()] != lastChangeCounter) {
print("FORCE reload");
_loadAsync(updateOpenStatus: true);
lastChangeCounter = changeCounter[widget.user.userId.toInt()]!;
}

View file

@ -35,7 +35,7 @@ class _ChatListViewState extends State<ChatListView> {
context.watch<MessagesChangeProvider>().lastMessage;
List<Contact> allUsers = context
.read<ContactChangeProvider>()
.watch<ContactChangeProvider>()
.allContacts
.where((c) => c.accepted)
.toList();
@ -106,20 +106,20 @@ class _ChatListViewState extends State<ChatListView> {
child: Padding(
padding: const EdgeInsets.all(10),
child: OutlinedButton.icon(
icon: Icon((activeUsers.isEmpty)
icon: Icon((allUsers.isEmpty)
? Icons.person_add
: Icons.camera_alt),
onPressed: () {
(activeUsers.isEmpty)
(allUsers.isEmpty)
? Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchUsernameView(),
),
)
: globalUpdateOfHomeViewPageIndex(1);
: globalUpdateOfHomeViewPageIndex(0);
},
label: Text((activeUsers.isEmpty)
label: Text((allUsers.isEmpty)
? context.lang.chatListViewSearchUserNameBtn
: context.lang.chatListViewSendFirstTwonly)),
),

View file

@ -10,6 +10,7 @@ import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/services/notification_service.dart';
final _noScreenshot = NoScreenshot.instance;
@ -69,6 +70,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}
}
flutterLocalNotificationsPlugin.cancel(widget.message.messageId);
List<int> token = content.downloadToken;
_imageByte =
await getDownloadedMedia(token, widget.message.messageOtherId!);
@ -202,7 +204,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
],
),
),
if (_imageByte != null)
if (_imageByte != null && false)
Positioned(
bottom: 30,
left: 0,

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:twonly/src/components/alert_dialog.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:provider/provider.dart';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/components/headline.dart';
import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/model/contacts_model.dart';

View file

@ -1,6 +1,7 @@
import 'package:introduction_screen/introduction_screen.dart';
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import 'package:twonly/src/utils/misc.dart';
// Slide 1: Welcome to [App Name]
// Text: "Experience a new way to connect with friends through secure, spontaneous image sharing."
@ -29,23 +30,21 @@ class OnboardingView extends StatelessWidget {
bodyPadding: EdgeInsets.only(top: 75, left: 10, right: 10),
pages: [
PageViewModel(
title: "Welcome to twonly!",
body:
"Experience a new way to connect with friends through secure, spontaneous image sharing.",
title: context.lang.onboardingWelcomeTitle,
body: context.lang.onboardingWelcomeBody,
image: Center(
child: Padding(
padding: const EdgeInsets.only(top: 100),
// child: Image.asset('assets/animations/messages.gif'),
child: Lottie.asset(
'assets/animations/messages.json',
'assets/animations/selfie2.json',
),
),
),
),
PageViewModel(
title: "End-to-End Encryption",
body:
"Your privacy matters. Enjoy peace of mind with end-to-end encryption, ensuring only you and your friends can see your images.",
title: context.lang.onboardingE2eTitle,
body: context.lang.onboardingE2eBody,
image: Center(
child: Padding(
padding: const EdgeInsets.only(top: 100),
@ -57,22 +56,19 @@ class OnboardingView extends StatelessWidget {
),
),
PageViewModel(
title: "Focus on sharing moments",
body:
"Say goodbye to addictive features! Our app is designed for sharing moments, no useless distractions or ads.",
title: context.lang.onboardingFocusTitle,
body: context.lang.onboardingFocusBody,
image: Center(
child: Padding(
padding: const EdgeInsets.only(top: 100),
child: Lottie.asset(
'assets/animations/selfie2.json',
),
child: Lottie.asset('assets/animations/takephoto.json',
repeat: false),
),
),
),
PageViewModel(
title: "Send twonlies",
body:
"Share moments securely with just one other person. twonly ensures that only you and your chosen friend can view the picture, keeping your moments private.",
title: context.lang.onboardingSendTwonliesTitle,
body: context.lang.onboardingSendTwonliesBody,
image: Center(
child: Padding(
padding: const EdgeInsets.only(top: 100),
@ -84,44 +80,40 @@ class OnboardingView extends StatelessWidget {
),
),
PageViewModel(
title: "You are not the product!",
body:
"If you don't pay, your data is the product that is sold. So we decided to develop a sustainable business model where everyone wins. You can keep your data private and we can create a beautiful app.",
title: context.lang.onboardingNotProductTitle,
body: context.lang.onboardingNotProductBody,
image: Center(
child: Padding(
padding: const EdgeInsets.only(top: 100),
child: Lottie.asset(
'assets/animations/product.json',
child: Image.asset(
'assets/images/onboarding/ricky_the_greedy_racoon.png',
),
),
),
),
PageViewModel(
title: "Pricing",
title: context.lang.onboardingBuyOneGetTwoTitle,
bodyWidget: Column(
children: [
Text(
"To be able to create a sustainable privacy focused app which does not show ads, we have to rely on you! You can get twonly for only 0,99€ / monthly or 9,99€ / yearly. As twonly is for at least two, you get a second user for free, so your twonly partner does not have to pay!",
context.lang.onboardingBuyOneGetTwoBody,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
],
),
image: Center(
child: Padding(
padding: const EdgeInsets.only(top: 100),
child: Lottie.asset(
'assets/animations/selfie.json',
),
child: Lottie.asset(
'assets/animations/present.lottie.json',
),
),
),
PageViewModel(
title: "Let's get started!",
title: context.lang.onboardingGetStartedTitle,
bodyWidget: Column(
children: [
Text(
"You can test twonly free for 14 days and then decide if it is worth to you.",
context.lang.onboardingGetStartedBody,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
@ -133,7 +125,7 @@ class OnboardingView extends StatelessWidget {
callbackOnSuccess();
// On button pressed
},
child: const Text("Try for free"),
child: Text(context.lang.onboardingTryForFree),
)),
],
),
@ -148,8 +140,8 @@ class OnboardingView extends StatelessWidget {
),
],
showNextButton: true,
done: const Text("Our plans"),
next: const Text("Next"),
done: Text("Our plans"),
next: Text(context.lang.next),
// done: RegisterView(callbackOnSuccess: callbackOnSuccess),
onDone: () {
callbackOnSuccess();

View file

@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:twonly/main.dart';
import 'package:twonly/globals.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:twonly/src/components/alert_dialog.dart';

View file

@ -318,6 +318,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_foreground_task:
dependency: "direct main"
description:
name: flutter_foreground_task
sha256: "206017ee1bf864f34b8d7bce664a172717caa21af8da23f55866470dfe316644"
url: "https://pub.dev"
source: hosted
version: "8.17.0"
flutter_image_compress:
dependency: "direct main"
description:
@ -1101,6 +1109,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f"
url: "https://pub.dev"
source: hosted
version: "2.4.4"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:

View file

@ -20,6 +20,7 @@ dependencies:
fixnum: ^1.1.1
flutter:
sdk: flutter
flutter_foreground_task: ^8.17.0
flutter_image_compress: ^2.4.0
flutter_local_notifications: ^18.0.1
flutter_localizations:
@ -82,9 +83,11 @@ flutter:
- assets/images/logo.jpg
- assets/
- assets/images/
- assets/images/onboarding/ricky_the_greedy_racoon.png
- assets/animations/present.lottie.json
- assets/animations/selfie2.json
- assets/animations/selfie.json
- assets/animations/messages.json
- assets/animations/takephoto.json
- assets/animations/local.json
- assets/animations/test.json
- assets/animations/product.json