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 {
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,
);
}

View file

@ -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};
}
}

View file

@ -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.",

View file

@ -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.",

View file

@ -125,7 +125,7 @@ Future notifyContactAboutOpeningMessage(
);
}
Future notifyContactsAboutAvatarChange() async {
Future notifyContactsAboutProfileChange() async {
List<Contact> 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(),
),
);

View file

@ -199,12 +199,15 @@ Future<client.Response> 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;

View file

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

View file

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

View file

@ -134,21 +134,22 @@ Future<String?> 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: <Widget>[
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

View file

@ -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<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
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Your Avatar"),
centerTitle: true,
title: Text(context.lang.settingsProfile),
),
body: ListView(
physics: BouncingScrollPhysics(),
children: <Widget>[
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<AvatarCreator> {
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<AvatarCreator> {
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<AvatarCreator> {
}
}
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<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/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<ProfileView> createState() => _ProfileViewState();
State<SettingsMainView> createState() => _SettingsMainViewState();
}
class _ProfileViewState extends State<ProfileView> {
class _SettingsMainViewState extends State<SettingsMainView> {
UserData? userData;
@override
@ -46,37 +46,41 @@ class _ProfileViewState extends State<ProfileView> {
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(