This commit is contained in:
otsmr 2025-03-22 12:10:38 +01:00
parent ec095fea9c
commit 01a24cc6dc
11 changed files with 180 additions and 76 deletions

View file

@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class BetterListTile extends StatelessWidget { class BetterListTile extends StatelessWidget {
final IconData icon; final IconData icon;
final String text; final String text;
final Widget? subtitle;
final Color? color; final Color? color;
final VoidCallback onTap; final VoidCallback onTap;
@ -12,6 +13,7 @@ class BetterListTile extends StatelessWidget {
required this.icon, required this.icon,
required this.text, required this.text,
this.color, this.color,
this.subtitle,
required this.onTap, required this.onTap,
}); });
@ -33,6 +35,7 @@ class BetterListTile extends StatelessWidget {
text, text,
style: TextStyle(color: color), style: TextStyle(color: color),
), ),
subtitle: subtitle,
onTap: onTap, onTap: onTap,
); );
} }

View file

@ -4,7 +4,7 @@ enum MessageKind {
textMessage, textMessage,
media, media,
contactRequest, contactRequest,
avatarChange, profileChange,
rejectRequest, rejectRequest,
acceptRequest, acceptRequest,
opened, opened,
@ -94,8 +94,8 @@ class MessageContent {
return MediaMessageContent.fromJson(json); return MediaMessageContent.fromJson(json);
case MessageKind.textMessage: case MessageKind.textMessage:
return TextMessageContent.fromJson(json); return TextMessageContent.fromJson(json);
case MessageKind.avatarChange: case MessageKind.profileChange:
return AvatarContent.fromJson(json); return ProfileContent.fromJson(json);
default: default:
return null; return null;
} }
@ -172,16 +172,20 @@ class TextMessageContent extends MessageContent {
} }
} }
class AvatarContent extends MessageContent { class ProfileContent extends MessageContent {
String svg; String avatarSvg;
AvatarContent({required this.svg}); String displayName;
ProfileContent({required this.avatarSvg, required this.displayName});
static AvatarContent fromJson(Map json) { static ProfileContent fromJson(Map json) {
return AvatarContent(svg: json['svg']); return ProfileContent(
avatarSvg: json['avatarSvg'],
displayName: json['displayName'],
);
} }
@override @override
Map toJson() { Map toJson() {
return {'svg': svg}; return {'avatarSvg': avatarSvg, 'displayName': displayName};
} }
} }

View file

@ -51,6 +51,10 @@
"messageSendState_Loading": "Herunterladen", "messageSendState_Loading": "Herunterladen",
"imageEditorDrawOk": "Zeichnung machen", "imageEditorDrawOk": "Zeichnung machen",
"settingsTitle": "Einstellungen", "settingsTitle": "Einstellungen",
"settingsProfile": "Profil",
"settingsProfileCustomizeAvatar": "Avatar anpassen",
"settingsProfileEditDisplayName": "Anzeigename",
"settingsProfileEditDisplayNameNew": "Neuer Anzeigename",
"settingsAccount": "Konto", "settingsAccount": "Konto",
"settingsSubscription": "Abonnement", "settingsSubscription": "Abonnement",
"settingsAppearance": "Erscheinungsbild", "settingsAppearance": "Erscheinungsbild",
@ -74,6 +78,7 @@
"contactVerifyNumberClearVerification": "Verifizierung aufheben", "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.", "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", "contactNickname": "Spitzname",
"contactNicknameNew": "Neuer Spitzname",
"contactBlock": "Blockieren", "contactBlock": "Blockieren",
"contactBlockTitle": "Blockiere {username}", "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.", "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.",

View file

@ -52,6 +52,10 @@
"messageSendState_Loading": "Downloading", "messageSendState_Loading": "Downloading",
"imageEditorDrawOk": "Take drawing", "imageEditorDrawOk": "Take drawing",
"settingsTitle": "Settings", "settingsTitle": "Settings",
"settingsProfile": "Profile",
"settingsProfileCustomizeAvatar": "Customize your avatar",
"settingsProfileEditDisplayName": "Displayname",
"settingsProfileEditDisplayNameNew": "New Displayname",
"settingsAccount": "Konto", "settingsAccount": "Konto",
"settingsSubscription": "Subscription", "settingsSubscription": "Subscription",
"settingsAppearance": "Appearance", "settingsAppearance": "Appearance",
@ -75,6 +79,7 @@
"contactVerifyNumberClearVerification": "Clear verification", "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.", "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", "contactNickname": "Nickname",
"contactNicknameNew": "New nickname",
"contactBlock": "Block", "contactBlock": "Block",
"contactBlockTitle": "Block {username}", "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.", "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.",

View file

@ -125,7 +125,7 @@ Future notifyContactAboutOpeningMessage(
); );
} }
Future notifyContactsAboutAvatarChange() async { Future notifyContactsAboutProfileChange() async {
List<Contact> contacts = List<Contact> contacts =
await twonlyDatabase.contactsDao.getAllNotBlockedContacts(); await twonlyDatabase.contactsDao.getAllNotBlockedContacts();
@ -140,8 +140,11 @@ Future notifyContactsAboutAvatarChange() async {
null, null,
contact.userId, contact.userId,
MessageJson( MessageJson(
kind: MessageKind.avatarChange, kind: MessageKind.profileChange,
content: AvatarContent(svg: user.avatarSvg!), content: ProfileContent(
avatarSvg: user.avatarSvg!,
displayName: user.displayName,
),
timestamp: DateTime.now(), timestamp: DateTime.now(),
), ),
); );

View file

@ -199,12 +199,15 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
final update = ContactsCompanion(accepted: Value(true)); final update = ContactsCompanion(accepted: Value(true));
twonlyDatabase.contactsDao.updateContact(fromUserId, update); twonlyDatabase.contactsDao.updateContact(fromUserId, update);
localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888); localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888);
notifyContactsAboutAvatarChange(); notifyContactsAboutProfileChange();
break; break;
case MessageKind.avatarChange: case MessageKind.profileChange:
var content = message.content; var content = message.content;
if (content is AvatarContent) { if (content is ProfileContent) {
final update = ContactsCompanion(avatarSvg: Value(content.svg)); final update = ContactsCompanion(
avatarSvg: Value(content.avatarSvg),
displayName: Value(content.displayName),
);
twonlyDatabase.contactsDao.updateContact(fromUserId, update); twonlyDatabase.contactsDao.updateContact(fromUserId, update);
} }
break; break;

View file

@ -75,7 +75,7 @@ class ApiProvider {
if (!globalIsAppInBackground) { if (!globalIsAppInBackground) {
tryTransmitMessages(); tryTransmitMessages();
tryDownloadAllMediaFiles(); tryDownloadAllMediaFiles();
notifyContactsAboutAvatarChange(); notifyContactsAboutProfileChange();
} }
} }

View file

@ -41,7 +41,7 @@ class _ChatListViewState extends State<ChatListView> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ProfileView(), builder: (context) => SettingsMainView(),
), ),
); );
}, },
@ -77,7 +77,7 @@ class _ChatListViewState extends State<ChatListView> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ProfileView(), builder: (context) => SettingsMainView(),
), ),
); );
}, },

View file

@ -134,21 +134,22 @@ Future<String?> showNicknameChangeDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text('Change nickname'), title: Text(context.lang.contactNickname),
content: TextField( content: TextField(
controller: controller, controller: controller,
autofocus: true, autofocus: true,
decoration: InputDecoration(hintText: "New nickname"), decoration:
InputDecoration(hintText: context.lang.contactNicknameNew),
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: Text('Cancel'), child: Text(context.lang.cancel),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); // Close the dialog Navigator.of(context).pop(); // Close the dialog
}, },
), ),
TextButton( TextButton(
child: Text('Submit'), child: Text(context.lang.ok),
onPressed: () { onPressed: () {
Navigator.of(context) Navigator.of(context)
.pop(controller.text); // Return the input text .pop(controller.text); // Return the input text

View file

@ -2,40 +2,66 @@ import 'dart:math';
import 'package:avatar_maker/avatar_maker.dart'; import 'package:avatar_maker/avatar_maker.dart';
import 'package:flutter/material.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/json_models/userdata.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
class AvatarCreator extends StatefulWidget { class ProfileView extends StatefulWidget {
const AvatarCreator({super.key}); const ProfileView({super.key});
@override @override
State<AvatarCreator> createState() => _AvatarCreatorState(); State<ProfileView> createState() => _ProfileViewState();
} }
class _AvatarCreatorState extends State<AvatarCreator> { class _ProfileViewState extends State<ProfileView> {
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text("Your Avatar"), title: Text(context.lang.settingsProfile),
centerTitle: true,
), ),
body: ListView( body: ListView(
physics: BouncingScrollPhysics(), physics: BouncingScrollPhysics(),
children: <Widget>[ children: <Widget>[
SizedBox(
height: 25,
),
SizedBox( SizedBox(
height: 25, height: 25,
), ),
AvatarMakerAvatar( AvatarMakerAvatar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
radius: 100, radius: 80,
), ),
SizedBox( SizedBox(
height: 25, height: 10,
), ),
Row( Row(
children: [ children: [
@ -46,11 +72,11 @@ class _AvatarCreatorState extends State<AvatarCreator> {
height: 35, height: 35,
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: Icon(Icons.edit), icon: Icon(Icons.edit),
label: Text("Customize"), label: Text(context.lang.settingsProfileCustomizeAvatar),
onPressed: () => Navigator.push( onPressed: () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => NewPage(), builder: (context) => ModifyAvatar(),
), ),
), ),
), ),
@ -59,8 +85,20 @@ class _AvatarCreatorState extends State<AvatarCreator> {
Spacer(flex: 2), Spacer(flex: 2),
], ],
), ),
SizedBox( SizedBox(height: 20),
height: 100, 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<AvatarCreator> {
} }
} }
class NewPage extends StatelessWidget { class ModifyAvatar extends StatelessWidget {
const NewPage({super.key}); const ModifyAvatar({super.key});
Future updateUserAvatar(String json, String svg) async { Future updateUserAvatar(String json, String svg) async {
UserData? user = await getUser(); UserData? user = await getUser();
@ -83,14 +121,16 @@ class NewPage extends StatelessWidget {
user.avatarCounter = user.avatarCounter! + 1; user.avatarCounter = user.avatarCounter! + 1;
} }
await updateUser(user); await updateUser(user);
await notifyContactsAboutAvatarChange(); await notifyContactsAboutProfileChange();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var _width = MediaQuery.of(context).size.width; var _width = MediaQuery.of(context).size.width;
return Scaffold( return Scaffold(
appBar: AppBar(), appBar: AppBar(
title: Text(context.lang.settingsProfileCustomizeAvatar),
),
body: Center( body: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
@ -158,3 +198,39 @@ class NewPage extends StatelessWidget {
); );
} }
} }
Future<String?> showDisplayNameChangeDialog(
BuildContext context, String currentName) {
final TextEditingController controller =
TextEditingController(text: currentName);
return showDialog<String>(
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: <Widget>[
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
},
),
],
);
},
);
}

View file

@ -7,18 +7,18 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/account_view.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/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/help_view.dart';
import 'package:twonly/src/views/settings/privacy_view.dart'; import 'package:twonly/src/views/settings/privacy_view.dart';
class ProfileView extends StatefulWidget { class SettingsMainView extends StatefulWidget {
const ProfileView({super.key}); const SettingsMainView({super.key});
@override @override
State<ProfileView> createState() => _ProfileViewState(); State<SettingsMainView> createState() => _SettingsMainViewState();
} }
class _ProfileViewState extends State<ProfileView> { class _SettingsMainViewState extends State<SettingsMainView> {
UserData? userData; UserData? userData;
@override @override
@ -46,37 +46,41 @@ class _ProfileViewState extends State<ProfileView> {
padding: const EdgeInsets.all(30), padding: const EdgeInsets.all(30),
child: Row( child: Row(
children: [ children: [
GestureDetector(
onTap: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) {
return AvatarCreator();
}));
initAsync();
},
child: ContactAvatar(
userData: userData!,
fontSize: 30,
),
),
SizedBox(width: 20),
Expanded( Expanded(
child: Column( child: GestureDetector(
crossAxisAlignment: CrossAxisAlignment.start, onTap: () async {
children: [ await Navigator.push(context,
Text( MaterialPageRoute(builder: (context) {
userData!.displayName, return ProfileView();
style: TextStyle(fontSize: 20), }));
textAlign: TextAlign.left, initAsync();
), },
Text( child: Row(
userData!.username, children: [
style: TextStyle( ContactAvatar(
fontSize: 14, 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( Align(