contact view page

This commit is contained in:
otsmr 2025-02-08 18:15:57 +01:00
parent 7291f1656c
commit e7a4b59379
14 changed files with 324 additions and 118 deletions

View file

@ -4,11 +4,11 @@ Don't be lonely, get twonly! Send pictures to a friend in real time and be sure
## TODOS bevor first beta ## TODOS bevor first beta
- Pro Invitation codes
- Push Notification (Android)
- Contact view page
- Verify contact view - Verify contact view
- Context Menu - Context Menu
- Pro Invitation codes
- Push Notification (Android)
- Settings - Settings
- Notification - Notification
- Real deployment aufsetzen, direkt auf Netcup? - Real deployment aufsetzen, direkt auf Netcup?

View file

@ -0,0 +1,44 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart';
Future<bool> showAlertDialog(
BuildContext context, String title, String content) async {
Completer<bool> completer = Completer<bool>();
Widget okButton = TextButton(
child: Text(context.lang.ok),
onPressed: () {
completer.complete(true);
Navigator.pop(context);
},
);
Widget cancelButton = TextButton(
child: Text(context.lang.cancel),
onPressed: () {
completer.complete(false);
Navigator.pop(context);
},
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(title),
content: Text(content),
actions: [
cancelButton,
okButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
return completer.future;
}

View file

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class BetterListTile extends StatelessWidget {
final IconData icon;
final String text;
final Color? color;
final VoidCallback onTap;
const BetterListTile({
super.key,
required this.icon,
required this.text,
this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Padding(
padding: const EdgeInsets.only(
right: 10,
left: 19,
),
child: FaIcon(
icon,
size: 20,
color: color,
),
),
title: Text(
text,
style: TextStyle(color: color),
),
onTap: onTap,
);
}
}

View file

@ -59,7 +59,6 @@
"addDrawing": "Drawing", "addDrawing": "Drawing",
"addEmoji": "Emoji", "addEmoji": "Emoji",
"toogleFlashLight": "Toggle the flash light", "toogleFlashLight": "Toggle the flash light",
"chatListDetailTitle": "Your chat with {username}",
"searchUsernameNotFoundLong": "\"{username}\" is not a twonly user. Please check the username and try again.", "searchUsernameNotFoundLong": "\"{username}\" is not a twonly user. Please check the username and try again.",
"errorUnknown": "An unexpected error has occurred. Please try again later.", "errorUnknown": "An unexpected error has occurred. Please try again later.",
"errorBadRequest": "The request could not be understood by the server due to malformed syntax. Please check your input and try again.", "errorBadRequest": "The request could not be understood by the server due to malformed syntax. Please check your input and try again.",

View file

@ -100,18 +100,6 @@ class DbContacts extends CvModelBase {
} }
} }
static Future _updateFlameCounter(int userId, int totalMediaCounter) async {
Map<String, dynamic> valuesToUpdate = {
columnTotalMediaCounter: totalMediaCounter
};
await dbProvider.db!.update(
tableName,
valuesToUpdate,
where: "$columnUserId = ?",
whereArgs: [userId],
);
}
static Future<List<Contact>> _getAllUsers() async { static Future<List<Contact>> _getAllUsers() async {
try { try {
var users = await dbProvider.db!.query(tableName, columns: [ var users = await dbProvider.db!.query(tableName, columns: [
@ -146,31 +134,45 @@ class DbContacts extends CvModelBase {
} }
} }
static Future blockUser(int userId, {bool unblock = false}) async { static Future _update(int userId, Map<String, dynamic> updates,
Map<String, dynamic> valuesToUpdate = { {bool notifyFlutter = true}) async {
columnBlocked: unblock ? 0 : 1,
};
await dbProvider.db!.update( await dbProvider.db!.update(
tableName, tableName,
valuesToUpdate, updates,
where: "$columnUserId = ?", where: "$columnUserId = ?",
whereArgs: [userId], whereArgs: [userId],
); );
if (notifyFlutter) {
globalCallBackOnContactChange(); globalCallBackOnContactChange();
} }
}
static Future changeDisplayName(int userId, String newDisplayName) async {
if (newDisplayName == "") return;
Map<String, dynamic> updates = {
columnDisplayName: newDisplayName,
};
await _update(userId, updates);
}
static Future _updateFlameCounter(int userId, int totalMediaCounter) async {
Map<String, dynamic> updates = {columnTotalMediaCounter: totalMediaCounter};
await _update(userId, updates, notifyFlutter: false);
}
static Future blockUser(int userId, {bool unblock = false}) async {
Map<String, dynamic> updates = {
columnBlocked: unblock ? 0 : 1,
};
await _update(userId, updates);
}
static Future acceptUser(int userId) async { static Future acceptUser(int userId) async {
Map<String, dynamic> valuesToUpdate = { Map<String, dynamic> updates = {
columnAccepted: 1, columnAccepted: 1,
columnRequested: 0, columnRequested: 0,
}; };
await dbProvider.db!.update( await _update(userId, updates);
tableName,
valuesToUpdate,
where: "$columnUserId = ?",
whereArgs: [userId],
);
globalCallBackOnContactChange();
} }
static Future deleteUser(int userId) async { static Future deleteUser(int userId) async {

View file

@ -40,8 +40,7 @@ class DbMessage {
bool get messageReceived => messageOtherId != null; bool get messageReceived => messageOtherId != null;
bool isMedia() { bool isMedia() {
return messageContent is TextMessageContent || return messageContent is MediaMessageContent;
messageContent is MediaMessageContent;
} }
MessageSendState getSendState() { MessageSendState getSendState() {
@ -378,9 +377,9 @@ class DbMessages extends CvModelBase {
jsonDecode(fromDb[i][columnMessageContentJson])); jsonDecode(fromDb[i][columnMessageContentJson]));
var tmp = content; var tmp = content;
if (tmp is TextMessageContent && messageOpenedAt != null) {
messageOpenedAt = DateTime.tryParse(fromDb[i][columnMessageOpenedAt]);
if (messageOpenedAt != null) { if (messageOpenedAt != null) {
messageOpenedAt = DateTime.tryParse(fromDb[i][columnMessageOpenedAt]);
if (tmp is TextMessageContent && messageOpenedAt != null) {
if ((DateTime.now()).difference(messageOpenedAt).inHours >= 24) { if ((DateTime.now()).difference(messageOpenedAt).inHours >= 24) {
deleteTextContent(fromDb[i][columnMessageId], tmp); deleteTextContent(fromDb[i][columnMessageId], tmp);
} }

View file

@ -180,10 +180,7 @@ class _ShareImageView extends State<ShareImageView> {
widget.isRealTwonly, widget.isRealTwonly,
widget.maxShowTime, widget.maxShowTime,
); );
Navigator.popUntil(context, (route) => route.isFirst);
// TODO: pop back to the HomeView page popUntil did not work. check later how to improve in case of pushing more then 2
Navigator.pop(context);
Navigator.pop(context);
globalUpdateOfHomeViewPageIndex(1); globalUpdateOfHomeViewPageIndex(1);
}, },
style: ButtonStyle( style: ButtonStyle(

View file

@ -2,6 +2,7 @@ import 'dart:collection';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/components/message_send_state_icon.dart'; import 'package:twonly/src/components/message_send_state_icon.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
@ -11,6 +12,7 @@ import 'package:twonly/src/providers/download_change_provider.dart';
import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/views/chats/media_viewer_view.dart'; import 'package:twonly/src/views/chats/media_viewer_view.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact_view.dart';
class ChatListEntry extends StatelessWidget { class ChatListEntry extends StatelessWidget {
const ChatListEntry(this.message, this.user, this.lastMessageFromSameUser, const ChatListEntry(this.message, this.user, this.lastMessageFromSameUser,
@ -187,7 +189,28 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
} }
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.chatListDetailTitle(widget.user.displayName)), title: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return ContactView(widget.user);
}));
},
child: Row(
children: [
InitialsAvatar(
displayName: widget.user.displayName,
fontSize: 19,
),
SizedBox(width: 10),
Expanded(
child: Container(
color: Colors.transparent,
child: Text(widget.user.displayName),
),
),
],
),
),
), ),
body: Column( body: Column(
children: [ children: [

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/components/alert_dialog.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/main.dart'; import 'package:twonly/main.dart';
@ -9,7 +10,6 @@ import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/providers/contacts_change_provider.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/views/onboarding/register_view.dart';
// ignore: library_prefixes // ignore: library_prefixes
import 'package:twonly/src/utils/signal.dart' as SignalHelper; import 'package:twonly/src/utils/signal.dart' as SignalHelper;

View file

@ -0,0 +1,32 @@
import 'package:twonly/src/model/contacts_model.dart';
import 'package:flutter/material.dart';
class ContactVerifyView extends StatefulWidget {
const ContactVerifyView(this.contact, {super.key});
final Contact contact;
@override
State<ContactVerifyView> createState() => _ContactVerifyViewState();
}
class _ContactVerifyViewState extends State<ContactVerifyView> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Verify ${widget.contact.displayName}"),
),
body: ListView(
children: [
SizedBox(height: 50),
],
),
);
}
}

View file

@ -0,0 +1,140 @@
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/components/alert_dialog.dart';
import 'package:twonly/src/components/better_list_title.dart';
import 'package:twonly/src/components/flame.dart';
import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/views/contact/contact_verify_view.dart';
class ContactView extends StatefulWidget {
const ContactView(this.contact, {super.key});
final Contact contact;
@override
State<ContactView> createState() => _ContactViewState();
}
class _ContactViewState extends State<ContactView> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
int flameCounter = context
.watch<MessagesChangeProvider>()
.flamesCounter[widget.contact.userId.toInt()] ??
0;
return Scaffold(
appBar: AppBar(
title: Text(""),
),
body: ListView(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: InitialsAvatar(
displayName: widget.contact.displayName,
fontSize: 30,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.contact.displayName,
style: TextStyle(fontSize: 20),
),
if (flameCounter > 0)
FlameCounterWidget(widget.contact, flameCounter, 110000000),
],
),
SizedBox(height: 50),
BetterListTile(
icon: FontAwesomeIcons.pencil,
text: "Nickname",
onTap: () async {
String? newUsername =
await showNicknameChangeDialog(context, widget.contact);
if (newUsername != null && newUsername != "") {
await DbContacts.changeDisplayName(
widget.contact.userId.toInt(), newUsername);
}
},
),
const Divider(),
BetterListTile(
icon: FontAwesomeIcons.shieldHeart,
text: "Verify safety number",
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return ContactVerifyView(widget.contact);
},
));
},
),
BetterListTile(
icon: FontAwesomeIcons.ban,
color: Colors.red,
text: "Block",
onTap: () async {
bool block = await showAlertDialog(
context,
"Block ${widget.contact.displayName}",
"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..");
if (block) {
await DbContacts.blockUser(widget.contact.userId.toInt());
// go back to the first
if (context.mounted) {
Navigator.popUntil(context, (route) => route.isFirst);
}
}
},
),
],
),
);
}
}
Future<String?> showNicknameChangeDialog(
BuildContext context, Contact contact) {
final TextEditingController controller =
TextEditingController(text: contact.displayName);
return showDialog<String>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Change nickname'),
content: TextField(
controller: controller,
autofocus: true,
decoration: InputDecoration(hintText: "New nickname"),
),
actions: <Widget>[
TextButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop(); // Close the dialog
},
),
TextButton(
child: Text('Submit'),
onPressed: () {
String inputText = controller.text;
Navigator.of(context).pop(inputText); // Return the input text
},
),
],
);
},
);
}

View file

@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
import 'package:twonly/main.dart'; import 'package:twonly/main.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:twonly/src/components/alert_dialog.dart';
import 'package:twonly/src/model/json/user_data.dart'; import 'package:twonly/src/model/json/user_data.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/signal.dart'; import 'package:twonly/src/utils/signal.dart';
@ -186,43 +187,3 @@ class _RegisterViewState extends State<RegisterView> {
); );
} }
} }
Future<bool> showAlertDialog(
BuildContext context, String title, String content) async {
Completer<bool> completer = Completer<bool>();
// set up the button
Widget okButton = TextButton(
child: Text(context.lang.ok),
onPressed: () {
completer.complete(true); // Complete the future with true
Navigator.pop(context);
},
);
Widget cancelButton = TextButton(
child: Text(context.lang.cancel),
onPressed: () {
completer.complete(false); // Complete the future with true
Navigator.pop(context);
},
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(title),
content: Text(content),
actions: [
cancelButton,
okButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
return completer.future;
}

View file

@ -1,8 +1,8 @@
import 'package:restart_app/restart_app.dart'; import 'package:restart_app/restart_app.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/components/alert_dialog.dart';
import 'package:twonly/src/utils/misc.dart'; 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/onboarding/register_view.dart';
class AccountView extends StatelessWidget { class AccountView extends StatelessWidget {
const AccountView({super.key}); const AccountView({super.key});

View file

@ -1,5 +1,6 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/components/better_list_title.dart';
import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/initialsavatar.dart';
import 'package:twonly/src/model/json/user_data.dart'; import 'package:twonly/src/model/json/user_data.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -79,7 +80,7 @@ class _ProfileViewState extends State<ProfileView> {
], ],
), ),
), ),
SettingsListTile( BetterListTile(
icon: FontAwesomeIcons.user, icon: FontAwesomeIcons.user,
text: context.lang.settingsAccount, text: context.lang.settingsAccount,
onTap: () { onTap: () {
@ -89,13 +90,13 @@ class _ProfileViewState extends State<ProfileView> {
})); }));
}, },
), ),
SettingsListTile( BetterListTile(
icon: FontAwesomeIcons.shieldHeart, icon: FontAwesomeIcons.shieldHeart,
text: context.lang.settingsSubscription, text: context.lang.settingsSubscription,
onTap: () {}, onTap: () {},
), ),
const Divider(), const Divider(),
SettingsListTile( BetterListTile(
icon: FontAwesomeIcons.sun, icon: FontAwesomeIcons.sun,
text: context.lang.settingsAppearance, text: context.lang.settingsAppearance,
onTap: () { onTap: () {
@ -105,7 +106,7 @@ class _ProfileViewState extends State<ProfileView> {
})); }));
}, },
), ),
SettingsListTile( BetterListTile(
icon: FontAwesomeIcons.lock, icon: FontAwesomeIcons.lock,
text: context.lang.settingsPrivacy, text: context.lang.settingsPrivacy,
onTap: () { onTap: () {
@ -115,7 +116,7 @@ class _ProfileViewState extends State<ProfileView> {
})); }));
}, },
), ),
SettingsListTile( BetterListTile(
icon: FontAwesomeIcons.bell, icon: FontAwesomeIcons.bell,
text: context.lang.settingsNotification, text: context.lang.settingsNotification,
onTap: () async { onTap: () async {
@ -140,7 +141,7 @@ class _ProfileViewState extends State<ProfileView> {
}, },
), ),
const Divider(), const Divider(),
SettingsListTile( BetterListTile(
icon: FontAwesomeIcons.circleQuestion, icon: FontAwesomeIcons.circleQuestion,
text: context.lang.settingsHelp, text: context.lang.settingsHelp,
onTap: () { onTap: () {
@ -156,34 +157,3 @@ class _ProfileViewState extends State<ProfileView> {
); );
} }
} }
class SettingsListTile extends StatelessWidget {
final IconData icon;
final String text;
final VoidCallback onTap;
const SettingsListTile({
super.key,
required this.icon,
required this.text,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Padding(
padding: const EdgeInsets.only(
right: 10,
left: 19,
),
child: FaIcon(
icon,
size: 20,
),
),
title: Text(text),
onTap: onTap,
);
}
}