diff --git a/lib/src/constants/routes.keys.dart b/lib/src/constants/routes.keys.dart index 9df0ea84..3113d38a 100644 --- a/lib/src/constants/routes.keys.dart +++ b/lib/src/constants/routes.keys.dart @@ -50,7 +50,6 @@ class Routes { static const String settingsHelpContactUs = '/settings/help/contact_us'; static const String settingsHelpNews = '/settings/help/news'; static const String settingsHelpDiagnostics = '/settings/help/diagnostics'; - static const String settingsHelpUserStudy = '/settings/help/user_study'; static const String settingsHelpUserStudyQuestionnaire = '/settings/help/user_study/questionnaire'; static const String settingsHelpCredits = '/settings/help/credits'; diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index 82f15b09..0f31236e 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -174,21 +174,6 @@ class UserData { // Used for push notifcation via FCM. String? fcmToken; - // For my master thesis I want to create a anonymous user study: - // - users in the "Tester" Plan can, if they want, take part of the user study - - @JsonKey(defaultValue: false) - bool askedForUserStudyPermission = false; - - // So update data can be assigned. If set the user choose to participate. - String? userStudyParticipantsToken; - - @JsonKey(defaultValue: 0) - int userStudyCountNewFriendsViaSuggestion = 0; - - // Once a day the anonymous data is collected and send to the server - DateTime? lastUserStudyDataUpload; - String? currentSetupPage; @JsonKey(defaultValue: false) diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart index 2b239d00..0f53018e 100644 --- a/lib/src/providers/routing.provider.dart +++ b/lib/src/providers/routing.provider.dart @@ -47,8 +47,6 @@ import 'package:twonly/src/visual/views/settings/profile/profile.view.dart'; import 'package:twonly/src/visual/views/settings/settings_main.view.dart'; import 'package:twonly/src/visual/views/settings/share_with_friends.view.dart'; import 'package:twonly/src/visual/views/settings/subscription/subscription.view.dart'; -import 'package:twonly/src/visual/views/user_study/user_study_questionnaire.view.dart'; -import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart'; final GlobalKey rootNavigatorKey = GlobalKey(); @@ -268,19 +266,6 @@ final routerProvider = GoRouter( path: 'diagnostics', builder: (context, state) => const DiagnosticsView(), ), - GoRoute( - path: 'user_study', - builder: (context, state) => UserStudyWelcomeView( - wasOpenedAutomatic: state.extra as bool? ?? false, - ), - routes: [ - GoRoute( - path: 'questionnaire', - builder: (context, state) => - const UserStudyQuestionnaireView(), - ), - ], - ), GoRoute( path: 'credits', builder: (context, state) => const CreditsView(), diff --git a/lib/src/services/api/api.service.dart b/lib/src/services/api/api.service.dart index c86d04da..4c918689 100644 --- a/lib/src/services/api/api.service.dart +++ b/lib/src/services/api/api.service.dart @@ -44,7 +44,6 @@ import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart'; -import 'package:twonly/src/services/user_study.service.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -147,11 +146,6 @@ class ApiService { unawaited(UserDiscoveryService.checkForNewAnnouncedUsers()); memoriesCloudService.init(); - - if (userService.currentUser.userStudyParticipantsToken != null) { - // In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection - unawaited(handleUserStudyUpload()); - } } } diff --git a/lib/src/services/user_study.service.dart b/lib/src/services/user_study.service.dart deleted file mode 100644 index ff738051..00000000 --- a/lib/src/services/user_study.service.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:http/http.dart' as http; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/database/tables/contacts.table.dart'; -import 'package:twonly/src/services/user.service.dart'; -import 'package:twonly/src/utils/keyvalue.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; - -const userStudySurveyKey = 'user_study_survey'; - -// PLEASE DO NOT SPAM OR TRY SENDING DIRECTLY TO THIS URL! -// You're just making my master's thesis more difficult and destroy scientific data. :/ -const surveyUrlBase = 'https://survey.twonly.org/upload.php'; - -Future handleUserStudyUpload() async { - try { - final token = userService.currentUser.userStudyParticipantsToken; - if (token == null) return; - - // in case the survey was taken offline try again - final userStudySurvey = await KeyValueStore.get(userStudySurveyKey); - if (userStudySurvey != null) { - final response = await http.post( - Uri.parse('$surveyUrlBase/create/$token'), - body: jsonEncode(userStudySurvey), - headers: {'Content-Type': 'application/json'}, - ); - if (response.statusCode != 200) { - Log.warn( - 'Got different status code for survey upload: ${response.statusCode}', - ); - return; - } - await KeyValueStore.delete(userStudySurveyKey); - } - - if (userService.currentUser.lastUserStudyDataUpload != null && - isToday(userService.currentUser.lastUserStudyDataUpload!)) { - // Only send updates once a day. - // This enables to see if improvements to actually work. - return; - } - - final contacts = await twonlyDB.contactsDao.getAllContacts(); - final verifications = await twonlyDB.keyVerificationDao - .getFirstVerificationTypeByContacts(); - final udVerifiedByContactsCount = await twonlyDB.keyVerificationDao - .getTransferredTrustVerificationsCount(); - - final totalContactsWithVerificationBadge = await twonlyDB.keyVerificationDao - .getCountOfContactsWithVerificationBadge(); - - final udFriendsShared = await twonlyDB.contactsDao - .getContactsAnnouncedViaUserDiscovery(); - - final udAllAnnouncedUsers = await twonlyDB.userDiscoveryDao - .getAllAnnouncedUsersWithRelations(); - - var udUnknownAnnouncedUsers = 0; - - for (final udUser in udAllAnnouncedUsers.keys) { - if (!contacts.any((c) => c.userId == udUser.announcedUserId)) { - udUnknownAnnouncedUsers += 1; - } - } - - final dataCollection = { - 'total_contacts': contacts.length, - - 'user_discovery_enabled': userService.currentUser.isUserDiscoveryEnabled, - 'user_discovery_required_send_images': - userService.currentUser.requiredSendImages, - 'user_discovery_threshold': - userService.currentUser.userDiscoveryThreshold, - 'user_discovery_requires_manual_approval': - userService.currentUser.userDiscoveryRequiresManualApproval, - 'user_discovery_share_promotion': - userService.currentUser.userDiscoverySharePromotion, - - 'user_discovery_count_friends_shared': udFriendsShared.length, - - 'user_discovery_count_announced_users': udAllAnnouncedUsers.length, - 'user_discovery_count_unknown_announced_users': udUnknownAnnouncedUsers, - - 'user_study_count_new_friends_via_suggestion': - userService.currentUser.userStudyCountNewFriendsViaSuggestion, - 'user_discovery_count_verified_by_contacts': udVerifiedByContactsCount, - - 'accepted_contacts': contacts.where((c) => c.accepted).length, - 'verified_contacts': verifications.length, - 'total_contacts_with_verification_badge': - totalContactsWithVerificationBadge, - 'verified_contacts_via_migrated_from_old_version': verifications.values - .where((c) => c == VerificationType.migratedFromOldVersion) - .length, - 'verified_contacts_via_qr_scanned': verifications.values - .where((c) => c == VerificationType.qrScanned) - .length, - 'verified_contacts_via_link': verifications.values - .where((c) => c == VerificationType.link) - .length, - 'verified_contacts_via_secret_qr_token': verifications.values - .where((c) => c == VerificationType.secretQrToken) - .length, - 'verified_contacts_via_contact_shared_by_verified': verifications.values - .where((c) => c == VerificationType.contactSharedByVerified) - .length, - }; - - final response = await http.post( - Uri.parse('$surveyUrlBase/push/$token'), - body: jsonEncode(dataCollection), - headers: {'Content-Type': 'application/json'}, - ); - if (response.statusCode == 200) { - await UserService.update((u) { - u.lastUserStudyDataUpload = DateTime.now(); - }); - } - if (response.statusCode == 404) { - // Token is unknown to the server... - await UserService.update((u) { - u - ..lastUserStudyDataUpload = null - ..userStudyParticipantsToken = null; - }); - } - } catch (e) { - if (e is http.ClientException || - e is SocketException || - e is TimeoutException) { - Log.warn('Error uploading user study data: $e'); - } else { - Log.error(e); - } - } -} diff --git a/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart b/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart index 2f00587c..e85ae286 100644 --- a/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart +++ b/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart @@ -7,7 +7,6 @@ import 'package:twonly/src/database/daos/user_discovery.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/utils.api.dart'; -import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; @@ -68,10 +67,6 @@ class FriendSuggestionsComp extends StatelessWidget { ); if (added > 0) await importSignalContactAndCreateRequest(userdata); - - await UserService.update( - (u) => u.userStudyCountNewFriendsViaSuggestion += 1, - ); } Future _hideAnnouncedUser(int userId) async { @@ -110,11 +105,14 @@ class FriendSuggestionsComp extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: friends.map((f) { final contact = f.$1; - final isSelected = - selectedFriends.contains(contact.userId); + final isSelected = selectedFriends.contains( + contact.userId, + ); return CheckboxListTile.adaptive( contentPadding: EdgeInsets.zero, - title: Text(contact.displayName ?? contact.username), + title: Text( + contact.displayName ?? contact.username, + ), value: isSelected, onChanged: (val) { setState(() { @@ -154,7 +152,7 @@ class FriendSuggestionsComp extends StatelessWidget { for (final contactId in selectedFriends) { await insertAndSendAskAboutUserMessage(contactId, user.announcedUserId); } - + await twonlyDB.userDiscoveryDao.updateAnnouncedUser( user.announcedUserId, const UserDiscoveryAnnouncedUsersCompanion( @@ -255,7 +253,8 @@ class FriendSuggestionsComp extends StatelessWidget { left: 4, ), ).merge(secondaryGreyButtonStyle(context)), - onPressed: () => _askFriends(context, user, friends), + onPressed: () => + _askFriends(context, user, friends), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/src/visual/views/settings/help/help.view.dart b/lib/src/visual/views/settings/help/help.view.dart index 7e65348c..0d120ab0 100644 --- a/lib/src/visual/views/settings/help/help.view.dart +++ b/lib/src/visual/views/settings/help/help.view.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; @@ -61,12 +60,6 @@ class _HelpViewState extends State { onTap: () => context.push(Routes.settingsHelpDiagnostics), ), const Divider(), - if (userService.currentUser.userStudyParticipantsToken == null || - kDebugMode) - ListTile( - title: const Text('Teilnahme an Nutzerstudie'), - onTap: () => context.push(Routes.settingsHelpUserStudy), - ), FutureBuilder( future: PackageInfo.fromPlatform(), builder: (context, snap) { diff --git a/lib/src/visual/views/user_study/user_study_questionnaire.view.dart b/lib/src/visual/views/user_study/user_study_questionnaire.view.dart deleted file mode 100644 index 53dae0ee..00000000 --- a/lib/src/visual/views/user_study/user_study_questionnaire.view.dart +++ /dev/null @@ -1,267 +0,0 @@ -// ignore_for_file: avoid_dynamic_calls - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:twonly/src/services/user.service.dart'; -import 'package:twonly/src/services/user_study.service.dart'; -import 'package:twonly/src/utils/keyvalue.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/components/snackbar.dart'; - -class UserStudyQuestionnaireView extends StatefulWidget { - const UserStudyQuestionnaireView({super.key}); - - @override - State createState() => - _UserStudyQuestionnaireViewState(); -} - -class _UserStudyQuestionnaireViewState - extends State { - final Map _responses = { - 'age': null, - 'education': null, - 'education_free': '', - 'vocational': null, - 'vocational_free': '', - 'enrolled': null, - 'study_program': '', - 'working': null, - 'work_field': '', - 'smartphone_2years': null, - 'comp_knowledge': null, - 'security_knowledge': null, - 'messengers': [], - 'is_release_mode': kReleaseMode, - }; - - final List _messengerOptions = [ - 'WhatsApp', - 'Signal', - 'Telegram', - 'Facebook Messenger', - 'iMessage', - 'Teams', - 'Viber', - 'Element', - 'Andere', - ]; - - Future _submitData() async { - await KeyValueStore.put(userStudySurveyKey, _responses); - - await UserService.update((u) { - // generate a random participants id to identify data send later while keeping the user anonym - u - ..userStudyParticipantsToken = getRandomString(25) - ..askedForUserStudyPermission = true; - }); - - await handleUserStudyUpload(); - - if (!mounted) return; - showSnackbar( - context, - 'Vielen Dank für deine Teilnahme!', - level: SnackbarLevel.success, - ); - context.pop(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Befragung')), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _sectionTitle('Demografische Daten'), - _questionText('Wie alt bist du?'), - _buildRadioList( - [ - '18-22', - '23-27', - '28-32', - '33-37', - '38-42', - '43-47', - '48-52', - '53-57', - '58-62', - '63-67', - '68-72', - '73-77', - '77 oder älter', - 'Keine Angabe', - ], - 'age', - ), - _questionText('Was ist dein höchster Schulabschluss?'), - _buildRadioList( - [ - 'Noch in der Schule', - 'Hauptschulabschluss', - 'POS (Polytechnische Oberschule)', - 'Realschulabschluss', - 'Abitur / Hochschulreife', - 'Kein Abschluss', - 'Keine Angabe', - ], - 'education', - ), - _buildTextField( - 'Freitext (optional)', - (val) => _responses['education_free'] = val, - ), - _questionText('Was ist dein höchster beruflicher Abschluss?'), - _buildRadioList( - [ - 'Berufsausbildung / Duales System', - 'Fachschulabschluss', - 'Fachschulabschluss (ehem. DDR)', - 'Bachelor', - 'Master', - 'Diplom', - 'Promotion (PhD)', - 'Kein beruflicher Abschluss', - 'Keine Angabe', - ], - 'vocational', - ), - _buildTextField( - 'Freitext (optional)', - (val) => _responses['vocational_free'] = val, - ), - _questionText( - 'Bist du derzeit in einem Studiengang eingeschrieben? (Bachelor, Master, Diplom, Staatsexamen, Magister)', - ), - _buildRadioList(['Ja', 'Nein', 'Keine Angabe'], 'enrolled'), - _questionText('Wenn ja, welcher Studiengang?'), - _buildTextField( - 'Studiengang eingeben', - (val) => _responses['study_program'] = val, - ), - _questionText('Bist du derzeit berufstätig?'), - _buildRadioList(['Ja', 'Nein', 'Keine Angabe'], 'working'), - _questionText('Wenn ja, in welchem Bereich arbeiten Sie?'), - _buildTextField( - 'Arbeitsbereich eingeben', - (val) => _responses['work_field'] = val, - ), - const SizedBox(height: 30), - // const Divider(), - _sectionTitle('Technisches Wissen'), - _questionText( - 'Nutzt du seit mehr als zwei Jahren ein Smartphone?', - ), - _buildRadioList(['Ja', 'Nein'], 'smartphone_2years'), - _questionText( - 'Wie schätzt du deine allgemeinen Computerkenntnisse ein?', - ), - _buildRadioList( - ['Anfänger', 'Mittel', 'Fortgeschritten'], - 'comp_knowledge', - ), - _questionText( - 'Wie schätzt du dein Wissen im Bereich IT-Sicherheit ein?', - ), - _buildRadioList( - ['Anfänger', 'Mittel', 'Fortgeschritten'], - 'security_knowledge', - ), - _questionText( - 'Welche der folgenden Messenger hast du schon einmal benutzt?', - ), - ..._messengerOptions.map( - (m) => CheckboxListTile.adaptive( - title: Text(m), - visualDensity: const VisualDensity(vertical: -4), - value: (_responses['messengers'] as List).contains(m), - onChanged: (value) { - setState(() { - value! - ? _responses['messengers'].add(m) - : _responses['messengers'].remove(m); - }); - }, - ), - ), - const SizedBox(height: 30), - Center( - child: FilledButton( - onPressed: _submitData, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15), - child: Text( - 'Jetzt teilnehmen', - style: TextStyle(fontSize: 18), - ), - ), - ), - ), - const SizedBox(height: 50), - ], - ), - ), - ); - } - - // Hilfsmethoden für das UI - Widget _sectionTitle(String title) => Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Text( - title, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ); - - Widget _questionText(String text) => Padding( - padding: const EdgeInsets.only(top: 20, bottom: 5), - child: Text( - text, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - ), - ); - - Widget _buildRadioList(List options, String key) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: DropdownButtonFormField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), - labelText: 'Bitte wählen...', - ), - initialValue: _responses[key] as String?, - items: options.map((value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - onChanged: (val) { - setState(() { - _responses[key] = val; - }); - }, - ), - ); - } - - Widget _buildTextField(String hint, void Function(String) onChanged) { - return TextField( - textCapitalization: TextCapitalization.sentences, - decoration: InputDecoration( - hintText: hint, - border: const OutlineInputBorder(), - ), - onChanged: onChanged, - ); - } -} diff --git a/lib/src/visual/views/user_study/user_study_welcome.view.dart b/lib/src/visual/views/user_study/user_study_welcome.view.dart deleted file mode 100644 index a1a5ce2b..00000000 --- a/lib/src/visual/views/user_study/user_study_welcome.view.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:twonly/src/constants/routes.keys.dart'; -import 'package:twonly/src/services/user.service.dart'; - -class UserStudyWelcomeView extends StatefulWidget { - const UserStudyWelcomeView({super.key, this.wasOpenedAutomatic = false}); - - final bool wasOpenedAutomatic; - - @override - State createState() => _UserStudyWelcomeViewState(); -} - -class _UserStudyWelcomeViewState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Teilnahme an Nutzerstudie'), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: ListView( - children: [ - const SizedBox(height: 30), - const Text( - 'Es dauert nur ein paar Minuten.', - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - ), - ), - const SizedBox(height: 20), - const Text( - 'Im Rahmen meiner Masterarbeit möchte ich die Benutzerfreundlichkeit von anonymen und dezentralen Messenger-Diensten verbessern.', - textAlign: TextAlign.center, - ), - const SizedBox(height: 10), - const Text( - 'Zu diesem Zweck werden in den nächsten Monaten verschiedene Änderungen an der App vorgenommen. Um die Wirksamkeit der Änderungen zu messen, möchte ich einige Daten über deine Nutzung der App sammeln sowie eine kurze Befragung durchführen.', - textAlign: TextAlign.center, - ), - const SizedBox(height: 10), - const Text( - 'Die Daten bestehen ausschließlich aus Zahlen, z. B. Anzahl deiner Kontakte. Alle Daten werden anonym übermittelt und können nicht mit deinem Benutzerkonto verknüpft werden.', - textAlign: TextAlign.center, - ), - const SizedBox(height: 10), - const Text( - 'Die Masterarbeit und damit die Nutzerstudie wird bis September durchgeführt. Nach Abschluss erhältst du eine Benachrichtigung und wirst über die Ergebnisse informiert.', - textAlign: TextAlign.center, - ), - const SizedBox(height: 40), - Center( - child: FilledButton( - onPressed: () => context.pushReplacement( - Routes.settingsHelpUserStudyQuestionnaire, - ), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15), - child: Text( - 'Weiter zur Befragung', - style: TextStyle(fontSize: 18), - ), - ), - ), - ), - const SizedBox(height: 10), - if (widget.wasOpenedAutomatic) - Center( - child: OutlinedButton( - onPressed: () => context.pop(), - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: Text( - 'Frag mich später noch mal', - style: TextStyle(fontSize: 18), - ), - ), - ), - ), - const SizedBox(height: 10), - if (widget.wasOpenedAutomatic) - Center( - child: GestureDetector( - onTap: () async { - await UserService.update( - (u) => u.askedForUserStudyPermission = true, - ); - if (context.mounted) context.pop(); - }, - child: const Text( - 'Nicht mehr anzeigen', - style: TextStyle(fontSize: 12), - ), - ), - ), - const SizedBox(height: 10), - const Text( - 'PS: twonly ist Open Source, wenn du also genau wissen willst, welche Daten übertragen werden, schau dir einfach die Datei "lib/src/views/user_study/user_study_data_collection.dart" im Repository an :).', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 10), - ), - ], - ), - ), - ); - } -}