twonly-app/lib/src/views/chats/add_new_user.view.dart
otsmr c3e92ac7d1
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
fix avatar render issues, fix list update issues
2025-11-18 22:56:42 +01:00

294 lines
9.1 KiB
Dart

import 'dart:async';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/headline.dart';
class AddNewUserView extends StatefulWidget {
const AddNewUserView({super.key});
@override
State<AddNewUserView> createState() => _SearchUsernameView();
}
class _SearchUsernameView extends State<AddNewUserView> {
final TextEditingController searchUserName = TextEditingController();
bool _isLoading = false;
bool hasRequestedUsers = false;
List<Contact> contacts = [];
late StreamSubscription<List<Contact>> contactsStream;
@override
void initState() {
super.initState();
contactsStream = twonlyDB.contactsDao.watchNotAcceptedContacts().listen(
(update) => setState(() {
contacts = update;
}),
);
}
@override
void dispose() {
unawaited(contactsStream.cancel());
super.dispose();
}
Future<void> _addNewUser(BuildContext context) async {
if (gUser.username == searchUserName.text) {
return;
}
setState(() {
_isLoading = true;
});
final userdata = await apiService.getUserData(searchUserName.text);
if (!context.mounted) return;
setState(() {
_isLoading = false;
});
if (userdata == null) {
await showAlertDialog(
context,
context.lang.searchUsernameNotFound,
context.lang.searchUsernameNotFoundBody(searchUserName.text),
);
return;
}
final addUser = await showAlertDialog(
context,
context.lang.userFound,
context.lang.userFoundBody,
);
if (!addUser || !context.mounted) {
return;
}
final added = await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion(
username: Value(searchUserName.text),
userId: Value(userdata.userId.toInt()),
requested: const Value(false),
blocked: const Value(false),
deletedByUser: const Value(false),
),
);
if (added > 0) {
if (await createNewSignalSession(userdata)) {
// 1. Setup notifications keys with the other user
await setupNotificationWithUsers(
forceContact: userdata.userId.toInt(),
);
// 2. Then send user request
await sendCipherText(
userdata.userId.toInt(),
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.REQUEST,
),
),
);
}
}
}
InputDecoration getInputDecoration(String hintText) {
return InputDecoration(
hintText: hintText,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(9),
borderSide: BorderSide(color: context.color.primary),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: context.color.outline),
),
contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.lang.searchUsernameTitle),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 10),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField(
onSubmitted: (_) async {
await _addNewUser(context);
},
onChanged: (value) {
searchUserName.text = value.toLowerCase();
searchUserName.selection = TextSelection.fromPosition(
TextPosition(offset: searchUserName.text.length),
);
},
inputFormatters: [
LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z]')),
],
controller: searchUserName,
decoration:
getInputDecoration(context.lang.searchUsernameInput),
),
),
const SizedBox(height: 20),
if (contacts.isNotEmpty)
HeadLineComponent(
context.lang.searchUsernameNewFollowerTitle,
),
Expanded(
child: ContactsListView(contacts),
),
],
),
),
),
floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30),
child: FloatingActionButton(
onPressed: _isLoading ? null : () async => _addNewUser(context),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: const FaIcon(FontAwesomeIcons.magnifyingGlassPlus),
),
),
);
}
}
class ContactsListView extends StatelessWidget {
const ContactsListView(this.contacts, {super.key});
final List<Contact> contacts;
List<Widget> sendRequestActions(BuildContext context, Contact contact) {
return [
Tooltip(
message: context.lang.searchUserNameArchiveUserTooltip,
child: IconButton(
icon: const FaIcon(Icons.archive_outlined, size: 15),
onPressed: () async {
const update = ContactsCompanion(requested: Value(false));
await twonlyDB.contactsDao.updateContact(contact.userId, update);
},
),
),
Text(context.lang.searchUserNamePending),
];
}
List<Widget> requestedActions(BuildContext context, Contact contact) {
return [
Tooltip(
message: context.lang.searchUserNameBlockUserTooltip,
child: IconButton(
icon: const Icon(
Icons.person_off_rounded,
color: Color.fromARGB(164, 244, 67, 54),
),
onPressed: () async {
const update = ContactsCompanion(blocked: Value(true));
await twonlyDB.contactsDao.updateContact(contact.userId, update);
},
),
),
Tooltip(
message: context.lang.searchUserNameRejectUserTooltip,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () async {
await sendCipherText(
contact.userId,
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.REJECT,
),
),
);
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(
accepted: Value(false),
requested: Value(false),
deletedByUser: Value(true),
),
);
},
),
),
IconButton(
icon: const Icon(Icons.check, color: Colors.green),
onPressed: () async {
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(
accepted: Value(true),
requested: Value(false),
),
);
await twonlyDB.groupsDao.createNewDirectChat(
contact.userId,
GroupsCompanion(
groupName: Value(getContactDisplayName(contact)),
),
);
await sendCipherText(
contact.userId,
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.ACCEPT,
),
),
);
await notifyContactsAboutProfileChange();
},
),
];
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) {
final contact = contacts[index];
return ListTile(
key: ValueKey(contact.userId),
title: Text(substringBy(contact.username, 25)),
leading: AvatarIcon(contactId: contact.userId),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: contact.requested
? requestedActions(context, contact)
: sendRequestActions(context, contact),
),
);
},
);
}
}