diff --git a/lib/src/components/better_list_title.dart b/lib/src/components/better_list_title.dart index c79c88a..67ed106 100644 --- a/lib/src/components/better_list_title.dart +++ b/lib/src/components/better_list_title.dart @@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class BetterListTile extends StatelessWidget { final IconData icon; final String text; + final Widget? subtitle; final Color? color; final VoidCallback onTap; @@ -12,6 +13,7 @@ class BetterListTile extends StatelessWidget { required this.icon, required this.text, this.color, + this.subtitle, required this.onTap, }); @@ -33,6 +35,7 @@ class BetterListTile extends StatelessWidget { text, style: TextStyle(color: color), ), + subtitle: subtitle, onTap: onTap, ); } diff --git a/lib/src/json_models/message.dart b/lib/src/json_models/message.dart index 95473eb..951c90c 100644 --- a/lib/src/json_models/message.dart +++ b/lib/src/json_models/message.dart @@ -4,7 +4,7 @@ enum MessageKind { textMessage, media, contactRequest, - avatarChange, + profileChange, rejectRequest, acceptRequest, opened, @@ -94,8 +94,8 @@ class MessageContent { return MediaMessageContent.fromJson(json); case MessageKind.textMessage: return TextMessageContent.fromJson(json); - case MessageKind.avatarChange: - return AvatarContent.fromJson(json); + case MessageKind.profileChange: + return ProfileContent.fromJson(json); default: return null; } @@ -172,16 +172,20 @@ class TextMessageContent extends MessageContent { } } -class AvatarContent extends MessageContent { - String svg; - AvatarContent({required this.svg}); +class ProfileContent extends MessageContent { + String avatarSvg; + String displayName; + ProfileContent({required this.avatarSvg, required this.displayName}); - static AvatarContent fromJson(Map json) { - return AvatarContent(svg: json['svg']); + static ProfileContent fromJson(Map json) { + return ProfileContent( + avatarSvg: json['avatarSvg'], + displayName: json['displayName'], + ); } @override Map toJson() { - return {'svg': svg}; + return {'avatarSvg': avatarSvg, 'displayName': displayName}; } } diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 2d88213..9c53921 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -51,6 +51,10 @@ "messageSendState_Loading": "Herunterladen", "imageEditorDrawOk": "Zeichnung machen", "settingsTitle": "Einstellungen", + "settingsProfile": "Profil", + "settingsProfileCustomizeAvatar": "Avatar anpassen", + "settingsProfileEditDisplayName": "Anzeigename", + "settingsProfileEditDisplayNameNew": "Neuer Anzeigename", "settingsAccount": "Konto", "settingsSubscription": "Abonnement", "settingsAppearance": "Erscheinungsbild", @@ -74,6 +78,7 @@ "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.", "contactNickname": "Spitzname", + "contactNicknameNew": "Neuer Spitzname", "contactBlock": "Blockieren", "contactBlockTitle": "Blockiere {username}", "contactBlockBody": "Ein blockierter Benutzer kann dir keine Nachrichten mehr senden, und sein Profil ist nicht mehr sichtbar. Um die Blockierung eines Benutzers aufzuheben, navigiere einfach zu Einstellungen > Datenschutz > Blockierte Benutzer.", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index da7cdc8..bfda4a8 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -52,6 +52,10 @@ "messageSendState_Loading": "Downloading", "imageEditorDrawOk": "Take drawing", "settingsTitle": "Settings", + "settingsProfile": "Profile", + "settingsProfileCustomizeAvatar": "Customize your avatar", + "settingsProfileEditDisplayName": "Displayname", + "settingsProfileEditDisplayNameNew": "New Displayname", "settingsAccount": "Konto", "settingsSubscription": "Subscription", "settingsAppearance": "Appearance", @@ -75,6 +79,7 @@ "contactVerifyNumberClearVerification": "Clear verification", "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.", "contactNickname": "Nickname", + "contactNicknameNew": "New nickname", "contactBlock": "Block", "contactBlockTitle": "Block {username}", "contactBlockBody": "A blocked user will no longer be able to send you messages and their profile will be hidden from view. To unblock a user, simply navigate to Settings > Privacy > Blocked Users.", diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart index fa4b985..5754da3 100644 --- a/lib/src/providers/api/api.dart +++ b/lib/src/providers/api/api.dart @@ -125,7 +125,7 @@ Future notifyContactAboutOpeningMessage( ); } -Future notifyContactsAboutAvatarChange() async { +Future notifyContactsAboutProfileChange() async { List contacts = await twonlyDatabase.contactsDao.getAllNotBlockedContacts(); @@ -140,8 +140,11 @@ Future notifyContactsAboutAvatarChange() async { null, contact.userId, MessageJson( - kind: MessageKind.avatarChange, - content: AvatarContent(svg: user.avatarSvg!), + kind: MessageKind.profileChange, + content: ProfileContent( + avatarSvg: user.avatarSvg!, + displayName: user.displayName, + ), timestamp: DateTime.now(), ), ); diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart index aa083cf..ea7be63 100644 --- a/lib/src/providers/api/server_messages.dart +++ b/lib/src/providers/api/server_messages.dart @@ -199,12 +199,15 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { final update = ContactsCompanion(accepted: Value(true)); twonlyDatabase.contactsDao.updateContact(fromUserId, update); localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888); - notifyContactsAboutAvatarChange(); + notifyContactsAboutProfileChange(); break; - case MessageKind.avatarChange: + case MessageKind.profileChange: var content = message.content; - if (content is AvatarContent) { - final update = ContactsCompanion(avatarSvg: Value(content.svg)); + if (content is ProfileContent) { + final update = ContactsCompanion( + avatarSvg: Value(content.avatarSvg), + displayName: Value(content.displayName), + ); twonlyDatabase.contactsDao.updateContact(fromUserId, update); } break; diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index d6277bd..340a92b 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -75,7 +75,7 @@ class ApiProvider { if (!globalIsAppInBackground) { tryTransmitMessages(); tryDownloadAllMediaFiles(); - notifyContactsAboutAvatarChange(); + notifyContactsAboutProfileChange(); } } diff --git a/lib/src/views/chats/chat_list_view.dart b/lib/src/views/chats/chat_list_view.dart index ed1e2ea..df0ad87 100644 --- a/lib/src/views/chats/chat_list_view.dart +++ b/lib/src/views/chats/chat_list_view.dart @@ -41,7 +41,7 @@ class _ChatListViewState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => ProfileView(), + builder: (context) => SettingsMainView(), ), ); }, @@ -77,7 +77,7 @@ class _ChatListViewState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => ProfileView(), + builder: (context) => SettingsMainView(), ), ); }, diff --git a/lib/src/views/contact/contact_view.dart b/lib/src/views/contact/contact_view.dart index 5cfac1b..c5fc2f6 100644 --- a/lib/src/views/contact/contact_view.dart +++ b/lib/src/views/contact/contact_view.dart @@ -134,21 +134,22 @@ Future showNicknameChangeDialog( context: context, builder: (BuildContext context) { return AlertDialog( - title: Text('Change nickname'), + title: Text(context.lang.contactNickname), content: TextField( controller: controller, autofocus: true, - decoration: InputDecoration(hintText: "New nickname"), + decoration: + InputDecoration(hintText: context.lang.contactNicknameNew), ), actions: [ TextButton( - child: Text('Cancel'), + child: Text(context.lang.cancel), onPressed: () { Navigator.of(context).pop(); // Close the dialog }, ), TextButton( - child: Text('Submit'), + child: Text(context.lang.ok), onPressed: () { Navigator.of(context) .pop(controller.text); // Return the input text diff --git a/lib/src/views/settings/avatar_creator.dart b/lib/src/views/settings/profile_view.dart similarity index 58% rename from lib/src/views/settings/avatar_creator.dart rename to lib/src/views/settings/profile_view.dart index 9a8995f..25eab30 100644 --- a/lib/src/views/settings/avatar_creator.dart +++ b/lib/src/views/settings/profile_view.dart @@ -2,40 +2,66 @@ import 'dart:math'; import 'package:avatar_maker/avatar_maker.dart'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/components/better_list_title.dart'; import 'package:twonly/src/json_models/userdata.dart'; import 'package:twonly/src/providers/api/api.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; -class AvatarCreator extends StatefulWidget { - const AvatarCreator({super.key}); +class ProfileView extends StatefulWidget { + const ProfileView({super.key}); @override - State createState() => _AvatarCreatorState(); + State createState() => _ProfileViewState(); } -class _AvatarCreatorState extends State { +class _ProfileViewState extends State { + UserData? user; + + @override + void initState() { + super.initState(); + initAsync(); + } + + Future initAsync() async { + user = await getUser(); + setState(() {}); + } + + Future updateUserDisplayname(String displayName) async { + UserData? user = await getUser(); + if (user == null) return null; + user.displayName = displayName; + if (user.avatarCounter == null) { + user.avatarCounter = 1; + } else { + user.avatarCounter = user.avatarCounter! + 1; + } + await updateUser(user); + await notifyContactsAboutProfileChange(); + initAsync(); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Your Avatar"), - centerTitle: true, + title: Text(context.lang.settingsProfile), ), body: ListView( physics: BouncingScrollPhysics(), children: [ - SizedBox( - height: 25, - ), SizedBox( height: 25, ), AvatarMakerAvatar( backgroundColor: Colors.transparent, - radius: 100, + radius: 80, ), SizedBox( - height: 25, + height: 10, ), Row( children: [ @@ -46,11 +72,11 @@ class _AvatarCreatorState extends State { height: 35, child: ElevatedButton.icon( icon: Icon(Icons.edit), - label: Text("Customize"), + label: Text(context.lang.settingsProfileCustomizeAvatar), onPressed: () => Navigator.push( context, MaterialPageRoute( - builder: (context) => NewPage(), + builder: (context) => ModifyAvatar(), ), ), ), @@ -59,8 +85,20 @@ class _AvatarCreatorState extends State { Spacer(flex: 2), ], ), - SizedBox( - height: 100, + SizedBox(height: 20), + const Divider(), + BetterListTile( + icon: FontAwesomeIcons.userPen, + text: context.lang.settingsProfileEditDisplayName, + subtitle: (user == null) ? null : Text(user!.displayName), + onTap: () async { + final displayName = + await showDisplayNameChangeDialog(context, user!.displayName); + + if (context.mounted && displayName != null && displayName != "") { + updateUserDisplayname(displayName); + } + }, ), ], ), @@ -68,8 +106,8 @@ class _AvatarCreatorState extends State { } } -class NewPage extends StatelessWidget { - const NewPage({super.key}); +class ModifyAvatar extends StatelessWidget { + const ModifyAvatar({super.key}); Future updateUserAvatar(String json, String svg) async { UserData? user = await getUser(); @@ -83,14 +121,16 @@ class NewPage extends StatelessWidget { user.avatarCounter = user.avatarCounter! + 1; } await updateUser(user); - await notifyContactsAboutAvatarChange(); + await notifyContactsAboutProfileChange(); } @override Widget build(BuildContext context) { var _width = MediaQuery.of(context).size.width; return Scaffold( - appBar: AppBar(), + appBar: AppBar( + title: Text(context.lang.settingsProfileCustomizeAvatar), + ), body: Center( child: SingleChildScrollView( child: Column( @@ -158,3 +198,39 @@ class NewPage extends StatelessWidget { ); } } + +Future showDisplayNameChangeDialog( + BuildContext context, String currentName) { + final TextEditingController controller = + TextEditingController(text: currentName); + + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(context.lang.settingsProfileEditDisplayName), + content: TextField( + controller: controller, + autofocus: true, + decoration: InputDecoration( + hintText: context.lang.settingsProfileEditDisplayNameNew), + ), + actions: [ + TextButton( + child: Text(context.lang.cancel), + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + }, + ), + TextButton( + child: Text(context.lang.ok), + onPressed: () { + Navigator.of(context) + .pop(controller.text); // Return the input text + }, + ), + ], + ); + }, + ); +} diff --git a/lib/src/views/settings/settings_main_view.dart b/lib/src/views/settings/settings_main_view.dart index 85125c7..cabfa1c 100644 --- a/lib/src/views/settings/settings_main_view.dart +++ b/lib/src/views/settings/settings_main_view.dart @@ -7,18 +7,18 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/account_view.dart'; import 'package:twonly/src/views/settings/appearance_view.dart'; -import 'package:twonly/src/views/settings/avatar_creator.dart'; +import 'package:twonly/src/views/settings/profile_view.dart'; import 'package:twonly/src/views/settings/help_view.dart'; import 'package:twonly/src/views/settings/privacy_view.dart'; -class ProfileView extends StatefulWidget { - const ProfileView({super.key}); +class SettingsMainView extends StatefulWidget { + const SettingsMainView({super.key}); @override - State createState() => _ProfileViewState(); + State createState() => _SettingsMainViewState(); } -class _ProfileViewState extends State { +class _SettingsMainViewState extends State { UserData? userData; @override @@ -46,37 +46,41 @@ class _ProfileViewState extends State { padding: const EdgeInsets.all(30), child: Row( children: [ - GestureDetector( - onTap: () async { - await Navigator.push(context, - MaterialPageRoute(builder: (context) { - return AvatarCreator(); - })); - initAsync(); - }, - child: ContactAvatar( - userData: userData!, - fontSize: 30, - ), - ), - SizedBox(width: 20), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - userData!.displayName, - style: TextStyle(fontSize: 20), - textAlign: TextAlign.left, - ), - Text( - userData!.username, - style: TextStyle( - fontSize: 14, + child: GestureDetector( + onTap: () async { + await Navigator.push(context, + MaterialPageRoute(builder: (context) { + return ProfileView(); + })); + initAsync(); + }, + child: Row( + children: [ + ContactAvatar( + userData: userData!, + fontSize: 30, ), - textAlign: TextAlign.left, - ), - ], + Container(width: 20, color: Colors.transparent), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + userData!.displayName, + style: TextStyle(fontSize: 20), + textAlign: TextAlign.left, + ), + Text( + userData!.username, + style: TextStyle( + fontSize: 14, + ), + textAlign: TextAlign.left, + ), + ], + ), + ], + ), ), ), Align(