feat(service): First attempt at handling phone contacts

This commit is contained in:
PapaTutuWawa 2022-12-10 19:34:11 +01:00
parent 73913c4ae6
commit e060b0f549
21 changed files with 347 additions and 31 deletions

View File

@ -48,6 +48,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />

View File

@ -442,6 +442,11 @@ files:
attributes: attributes:
deviceId: int deviceId: int
jid: String jid: String
- name: GetContactsCommandDebug
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
generate_builder: true generate_builder: true
# get${builder_Name}FromJson # get${builder_Name}FromJson
builder_name: "Command" builder_name: "Command"

View File

@ -27,7 +27,6 @@ String _cleanBase64String(String original) {
} }
class AvatarService { class AvatarService {
AvatarService() : _log = Logger('AvatarService'); AvatarService() : _log = Logger('AvatarService');
final Logger _log; final Logger _log;

122
lib/service/contact.dart Normal file
View File

@ -0,0 +1,122 @@
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxxyv2/service/conversation.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/roster.dart';
import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/shared/models/roster.dart';
class ContactWrapper {
const ContactWrapper(this.title, this.id, this.jid);
final String title;
final String id;
final String jid;
}
class ContactsService {
ContactsService() : _log = Logger('ContactsService') {
// NOTE: Apparently, this means that if false, contacts that are in 0 groups
// are not returned.
FlutterContacts.config.includeNonVisibleOnAndroid = true;
// Allow us to react to database changes
FlutterContacts.addListener(_onContactsDatabaseUpdate);
}
final Logger _log;
List<String>? _contactIds;
Future<List<ContactWrapper>> fetchContactsWithJabber() async {
final contacts = await FlutterContacts.getContacts(withProperties: true);
_log.finest('Got ${contacts.length} contacts');
final jabberContacts = List<ContactWrapper>.empty(growable: true);
for (final c in contacts) {
final index = c.socialMedias
.indexWhere((s) => s.label == SocialMediaLabel.jabber);
if (index == -1) continue;
jabberContacts.add(
ContactWrapper(
'${c.name.first} ${c.name.last}',
c.id,
c.socialMedias[index].userName,
),
);
}
_log.finest('${jabberContacts.length} contacts have an XMPP address');
return jabberContacts;
}
Future<void> _onContactsDatabaseUpdate() async {
_log.finest('Got contacts database update');
}
Future<List<String>> _getContactIds() async {
if (_contactIds != null) return _contactIds!;
_contactIds = List<String>.from(
await GetIt.I.get<DatabaseService>().getContactIds(),
);
return _contactIds!;
}
Future<void> scanContacts() async {
final db = GetIt.I.get<DatabaseService>();
final cs = GetIt.I.get<ConversationService>();
final rs = GetIt.I.get<RosterService>();
final contacts = await fetchContactsWithJabber();
final knownContactIds = await _getContactIds();
for (final id in knownContactIds) {
final index = contacts.indexWhere((c) => c.id == id);
if (index != -1) continue;
await db.removeContactId(id);
_contactIds!.remove(id);
}
final modifiedRosterItems = List<RosterItem>.empty(growable: true);
for (final contact in contacts) {
if (!knownContactIds.contains(contact.id)) {
await db.addContactId(contact.id);
_contactIds!.add(contact.id);
}
final c = await cs.getConversationByJid(contact.jid);
if (c != null) {
final newConv = await cs.updateConversation(
c.id,
contactId: contact.id,
);
sendEvent(
ConversationUpdatedEvent(
conversation: newConv,
),
);
}
final r = await rs.getRosterItemByJid(contact.jid);
if (r != null) {
final newRosterItem = await rs.updateRosterItem(
r.id,
contactId: contact.id,
);
modifiedRosterItems.add(newRosterItem);
} else {
// TODO(PapaTutuWawa): Create it
}
}
if (modifiedRosterItems.isNotEmpty) {
sendEvent(
RosterDiffEvent(
modified: modifiedRosterItems,
),
);
}
}
}

View File

@ -2,6 +2,7 @@ import 'package:get_it/get_it.dart';
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/database/database.dart'; import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/not_specified.dart';
import 'package:moxxyv2/service/preferences.dart'; import 'package:moxxyv2/service/preferences.dart';
import 'package:moxxyv2/shared/cache.dart'; import 'package:moxxyv2/shared/cache.dart';
import 'package:moxxyv2/shared/models/conversation.dart'; import 'package:moxxyv2/shared/models/conversation.dart';
@ -64,6 +65,7 @@ class ConversationService {
ChatState? chatState, ChatState? chatState,
bool? muted, bool? muted,
bool? encrypted, bool? encrypted,
Object? contactId = notSpecified,
}) async { }) async {
final conversation = (await _getConversationById(id))!; final conversation = (await _getConversationById(id))!;
var newConversation = await GetIt.I.get<DatabaseService>().updateConversation( var newConversation = await GetIt.I.get<DatabaseService>().updateConversation(
@ -76,6 +78,7 @@ class ConversationService {
chatState: conversation.chatState, chatState: conversation.chatState,
muted: muted, muted: muted,
encrypted: encrypted, encrypted: encrypted,
contactId: contactId,
); );
// Copy over the old lastMessage if a new one was not set // Copy over the old lastMessage if a new one was not set

View File

@ -11,6 +11,7 @@ const omemoTrustDeviceListTable = 'OmemoTrustDeviceList';
const omemoTrustEnableListTable = 'OmemoTrustEnableList'; const omemoTrustEnableListTable = 'OmemoTrustEnableList';
const omemoFingerprintCache = 'OmemoFingerprintCache'; const omemoFingerprintCache = 'OmemoFingerprintCache';
const xmppStateTable = 'XmppState'; const xmppStateTable = 'XmppState';
const contactsTable = 'Contacts';
const typeString = 0; const typeString = 0;
const typeInt = 1; const typeInt = 1;

View File

@ -62,19 +62,15 @@ Future<void> createDatabase(Database db, int version) async {
// Conversations // Conversations
await db.execute( await db.execute(
''' '''
CREATE TABLE $conversationsTable ( ''',
id INTEGER PRIMARY KEY AUTOINCREMENT, );
jid TEXT NOT NULL,
title TEXT NOT NULL, // Contacts
avatarUrl TEXT NOT NULL, await db.execute(
lastChangeTimestamp INTEGER NOT NULL, '''
unreadCounter INTEGER NOT NULL, CREATE TABLE $contactsTable (
open INTEGER NOT NULL, id TEXT PRIMARY KEY
muted INTEGER NOT NULL, )'''
encrypted INTEGER NOT NULL,
lastMessageId INTEGER,
CONSTRAINT fk_last_message FOREIGN KEY (lastMessageId) REFERENCES $messagesTable (id)
)''',
); );
// Shared media // Shared media
@ -102,7 +98,10 @@ Future<void> createDatabase(Database db, int version) async {
avatarUrl TEXT NOT NULL, avatarUrl TEXT NOT NULL,
avatarHash TEXT NOT NULL, avatarHash TEXT NOT NULL,
subscription TEXT NOT NULL, subscription TEXT NOT NULL,
ask TEXT NOT NULL ask TEXT NOT NULL,
contactId TEXT,
CONSTRAINT fk_contact_id FOREIGN KEY (contactId) REFERENCES $contactsTable (id)
ON DELETE SET NULL
)''', )''',
); );

View File

@ -8,6 +8,8 @@ import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/database/constants.dart'; import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/creation.dart'; import 'package:moxxyv2/service/database/creation.dart';
import 'package:moxxyv2/service/database/helpers.dart'; import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/service/database/migrations/0000_contacts_integration.dart';
import 'package:moxxyv2/service/database/migrations/0000_contacts_integration_fixup.dart';
import 'package:moxxyv2/service/database/migrations/0000_conversations.dart'; import 'package:moxxyv2/service/database/migrations/0000_conversations.dart';
import 'package:moxxyv2/service/database/migrations/0000_conversations2.dart'; import 'package:moxxyv2/service/database/migrations/0000_conversations2.dart';
import 'package:moxxyv2/service/database/migrations/0000_conversations3.dart'; import 'package:moxxyv2/service/database/migrations/0000_conversations3.dart';
@ -67,7 +69,7 @@ class DatabaseService {
_db = await openDatabase( _db = await openDatabase(
dbPath, dbPath,
password: key, password: key,
version: 13, version: 15,
onCreate: createDatabase, onCreate: createDatabase,
onConfigure: (db) async { onConfigure: (db) async {
// In order to do schema changes during database upgrades, we disable foreign // In order to do schema changes during database upgrades, we disable foreign
@ -128,6 +130,14 @@ class DatabaseService {
_log.finest('Running migration for database version 13'); _log.finest('Running migration for database version 13');
await upgradeFromV12ToV13(db); await upgradeFromV12ToV13(db);
} }
if (oldVersion < 14) {
_log.finest('Running migration for database version 14');
await upgradeFromV13ToV14(db);
}
if (oldVersion < 15) {
_log.finest('Running migration for database version 15');
await upgradeFromV14ToV15(db);
}
}, },
); );
@ -209,6 +219,7 @@ class DatabaseService {
ChatState? chatState, ChatState? chatState,
bool? muted, bool? muted,
bool? encrypted, bool? encrypted,
Object? contactId = notSpecified,
}) async { }) async {
final cd = (await _db.query( final cd = (await _db.query(
'Conversations', 'Conversations',
@ -245,6 +256,9 @@ class DatabaseService {
if (encrypted != null) { if (encrypted != null) {
c['encrypted'] = boolToInt(encrypted); c['encrypted'] = boolToInt(encrypted);
} }
if (contactId != notSpecified) {
c['contactId'] = contactId as String?;
}
await _db.update( await _db.update(
'Conversations', 'Conversations',
@ -636,10 +650,11 @@ class DatabaseService {
String? subscription, String? subscription,
String? ask, String? ask,
List<String>? groups, List<String>? groups,
Object? contactId = notSpecified,
} }
) async { ) async {
final id_ = (await _db.query( final id_ = (await _db.query(
'RosterItems', rosterTable,
where: 'id = ?', where: 'id = ?',
whereArgs: [id], whereArgs: [id],
limit: 1, limit: 1,
@ -666,9 +681,12 @@ class DatabaseService {
if (ask != null) { if (ask != null) {
i['ask'] = ask; i['ask'] = ask;
} }
if (contactId != notSpecified) {
i['contactId'] = contactId as String?;
}
await _db.update( await _db.update(
'RosterItems', rosterTable,
i, i,
where: 'id = ?', where: 'id = ?',
whereArgs: [id], whereArgs: [id],
@ -1042,4 +1060,27 @@ class DatabaseService {
}) })
.toList(); .toList();
} }
Future<List<String>> getContactIds() async {
return (await _db.query(contactsTable))
.map((item) => item['id']! as String)
.toList();
}
Future<void> addContactId(String id) async {
await _db.insert(
contactsTable,
<String, String>{
'id': id,
},
);
}
Future<void> removeContactId(String id) async {
await _db.delete(
contactsTable,
where: 'id = ?',
whereArgs: [id],
);
}
} }

View File

@ -0,0 +1,56 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV13ToV14(Database db) async {
// Create the new table
await db.execute(
'''
CREATE TABLE $contactsTable (
id TEXT PRIMARY KEY
)'''
);
// Migrate the conversations
await db.execute(
'''
CREATE TABLE ${conversationsTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
jid TEXT NOT NULL,
title TEXT NOT NULL,
avatarUrl TEXT NOT NULL,
lastChangeTimestamp INTEGER NOT NULL,
unreadCounter INTEGER NOT NULL,
open INTEGER NOT NULL,
muted INTEGER NOT NULL,
encrypted INTEGER NOT NULL,
lastMessageId INTEGER,
contactId TEXT,
CONSTRAINT fk_last_message FOREIGN KEY (lastMessageId) REFERENCES $messagesTable (id),
CONSTRAINT fk_contact_id FOREIGN KEY (contactId) REFERENCES $contactsTable (id)
ON DELETE SET NULL
)''',
);
await db.execute('INSERT INTO ${conversationsTable}_new SELECT *, NULL from $conversationsTable');
await db.execute('DROP TABLE $conversationsTable;');
await db.execute('ALTER TABLE ${conversationsTable}_new RENAME TO $conversationsTable;');
// Migrate the roster items
await db.execute(
'''
CREATE TABLE ${rosterTable}_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
jid TEXT NOT NULL,
title TEXT NOT NULL,
avatarUrl TEXT NOT NULL,
avatarHash TEXT NOT NULL,
subscription TEXT NOT NULL,
ask TEXT NOT NULL,
contactId TEXT,
CONSTRAINT fk_contact_id FOREIGN KEY (contactId) REFERENCES $contactsTable (id)
ON DELETE SET NULL
)''',
);
await db.execute('INSERT INTO ${rosterTable}_new SELECT *, NULL from $rosterTable');
await db.execute('DROP TABLE $rosterTable;');
await db.execute('ALTER TABLE ${rosterTable}_new RENAME TO $rosterTable;');
}

View File

@ -0,0 +1,15 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV14ToV15(Database db) async {
// Add the missing primary key
await db.execute(
'''
CREATE TABLE ${contactsTable}_new (
id TEXT PRIMARY KEY
)''',
);
await db.execute('INSERT INTO ${contactsTable}_new SELECT * from $contactsTable');
await db.execute('DROP TABLE $contactsTable;');
await db.execute('ALTER TABLE ${contactsTable}_new RENAME TO $contactsTable;');
}

View File

@ -8,6 +8,7 @@ import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/service/avatars.dart'; import 'package:moxxyv2/service/avatars.dart';
import 'package:moxxyv2/service/blocking.dart'; import 'package:moxxyv2/service/blocking.dart';
import 'package:moxxyv2/service/conversation.dart'; import 'package:moxxyv2/service/conversation.dart';
import 'package:moxxyv2/service/contact.dart';
import 'package:moxxyv2/service/database/database.dart'; import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/helpers.dart'; import 'package:moxxyv2/service/helpers.dart';
import 'package:moxxyv2/service/httpfiletransfer/helpers.dart'; import 'package:moxxyv2/service/httpfiletransfer/helpers.dart';
@ -70,6 +71,7 @@ void setupBackgroundEventHandler() {
EventTypeMatcher<AddReactionToMessageCommand>(performAddMessageReaction), EventTypeMatcher<AddReactionToMessageCommand>(performAddMessageReaction),
EventTypeMatcher<RemoveReactionFromMessageCommand>(performRemoveMessageReaction), EventTypeMatcher<RemoveReactionFromMessageCommand>(performRemoveMessageReaction),
EventTypeMatcher<MarkOmemoDeviceAsVerifiedCommand>(performMarkDeviceVerified), EventTypeMatcher<MarkOmemoDeviceAsVerifiedCommand>(performMarkDeviceVerified),
EventTypeMatcher<GetContactsCommandDebug>(performGetContacts),
]); ]);
GetIt.I.registerSingleton<EventHandler>(handler); GetIt.I.registerSingleton<EventHandler>(handler);
@ -744,3 +746,7 @@ Future<void> performMarkDeviceVerified(MarkOmemoDeviceAsVerifiedCommand command,
command.jid, command.jid,
); );
} }
Future<void> performGetContacts(GetContactsCommandDebug command, { dynamic extra }) async {
await GetIt.I.get<ContactsService>().scanContacts();
}

View File

@ -8,6 +8,7 @@ import 'package:moxplatform/moxplatform.dart';
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/conversation.dart'; import 'package:moxxyv2/service/conversation.dart';
import 'package:moxxyv2/service/database/database.dart'; import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/not_specified.dart';
import 'package:moxxyv2/service/service.dart'; import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/shared/events.dart'; import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/shared/models/conversation.dart'; import 'package:moxxyv2/shared/models/conversation.dart';
@ -223,6 +224,7 @@ class RosterService {
String? subscription, String? subscription,
String? ask, String? ask,
List<String>? groups, List<String>? groups,
Object? contactId = notSpecified,
} }
) async { ) async {
final newItem = await GetIt.I.get<DatabaseService>().updateRosterItem( final newItem = await GetIt.I.get<DatabaseService>().updateRosterItem(
@ -233,6 +235,7 @@ class RosterService {
subscription: subscription, subscription: subscription,
ask: ask, ask: ask,
groups: groups, groups: groups,
contactId: contactId,
); );
// Update cache // Update cache

View File

@ -13,6 +13,7 @@ import 'package:moxxyv2/service/avatars.dart';
import 'package:moxxyv2/service/blocking.dart'; import 'package:moxxyv2/service/blocking.dart';
import 'package:moxxyv2/service/connectivity.dart'; import 'package:moxxyv2/service/connectivity.dart';
import 'package:moxxyv2/service/connectivity_watcher.dart'; import 'package:moxxyv2/service/connectivity_watcher.dart';
import 'package:moxxyv2/service/contact.dart';
import 'package:moxxyv2/service/conversation.dart'; import 'package:moxxyv2/service/conversation.dart';
import 'package:moxxyv2/service/cryptography/cryptography.dart'; import 'package:moxxyv2/service/cryptography/cryptography.dart';
import 'package:moxxyv2/service/database/database.dart'; import 'package:moxxyv2/service/database/database.dart';
@ -153,6 +154,7 @@ Future<void> entrypoint() async {
GetIt.I.registerSingleton<MessageService>(MessageService()); GetIt.I.registerSingleton<MessageService>(MessageService());
GetIt.I.registerSingleton<OmemoService>(OmemoService()); GetIt.I.registerSingleton<OmemoService>(OmemoService());
GetIt.I.registerSingleton<CryptographyService>(CryptographyService()); GetIt.I.registerSingleton<CryptographyService>(CryptographyService());
GetIt.I.registerSingleton<ContactsService>(ContactsService());
final xmpp = XmppService(); final xmpp = XmppService();
GetIt.I.registerSingleton<XmppService>(xmpp); GetIt.I.registerSingleton<XmppService>(xmpp);

View File

@ -59,6 +59,10 @@ class Conversation with _$Conversation {
bool encrypted, bool encrypted,
// The current chat state // The current chat state
@ConversationChatStateConverter() ChatState chatState, @ConversationChatStateConverter() ChatState chatState,
{
// The id of the contact in the device's phonebook if it exists
String? contactId,
}
) = _Conversation; ) = _Conversation;
const Conversation._(); const Conversation._();

View File

@ -14,6 +14,10 @@ class RosterItem with _$RosterItem {
String subscription, String subscription,
String ask, String ask,
List<String> groups, List<String> groups,
{
// The id of the contact in the device's phonebook if it exists
String? contactId,
}
) = _RosterItem; ) = _RosterItem;
const RosterItem._(); const RosterItem._();

View File

@ -121,7 +121,8 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
state: s.toString().split('.').last, state: s.toString().split('.').last,
jid: state.conversation!.jid, jid: state.conversation!.jid,
), ),
awaitable: false,); awaitable: false,
);
} }
Future<void> _onInit(InitConversationEvent event, Emitter<ConversationState> emit) async { Future<void> _onInit(InitConversationEvent event, Emitter<ConversationState> emit) async {

View File

@ -15,6 +15,9 @@ import 'package:moxxyv2/ui/widgets/avatar.dart';
import 'package:moxxyv2/ui/widgets/conversation.dart'; import 'package:moxxyv2/ui/widgets/conversation.dart';
import 'package:moxxyv2/ui/widgets/overview_menu.dart'; import 'package:moxxyv2/ui/widgets/overview_menu.dart';
import 'package:moxxyv2/ui/widgets/topbar.dart'; import 'package:moxxyv2/ui/widgets/topbar.dart';
import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/commands.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
enum ConversationsOptions { enum ConversationsOptions {
settings settings
@ -127,6 +130,7 @@ class ConversationsPageState extends State<ConversationsPage> with TickerProvide
itemCount: state.conversations.length, itemCount: state.conversations.length,
itemBuilder: (_context, index) { itemBuilder: (_context, index) {
final item = state.conversations[index]; final item = state.conversations[index];
print('${item.jid} -> ${item.contactId}');
final row = ConversationsListRow( final row = ConversationsListRow(
maxTextWidth, maxTextWidth,
item, item,
@ -295,7 +299,16 @@ class ConversationsPageState extends State<ConversationsPage> with TickerProvide
children: [ children: [
SpeedDialChild( SpeedDialChild(
child: const Icon(Icons.group), child: const Icon(Icons.group),
onTap: () => showNotImplementedDialog('groupchat', context), onTap: () async {
final r = await FlutterContacts.requestPermission(readonly: true);
print(r);
if (!r) return;
await MoxplatformPlugin.handler.getDataSender().sendData(
GetContactsCommandDebug(),
awaitable: false,
);
},
backgroundColor: primaryColor, backgroundColor: primaryColor,
// TODO(Unknown): Theme dependent? // TODO(Unknown): Theme dependent?
foregroundColor: Colors.white, foregroundColor: Colors.white,

View File

@ -124,6 +124,7 @@ class NewConversationPage extends StatelessWidget {
false, false,
false, false,
ChatState.gone, ChatState.gone,
contactId: item.contactId,
), ),
false, false,
showTimestamp: false, showTimestamp: false,

View File

@ -1,6 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:badges/badges.dart'; import 'package:badges/badges.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:moxxyv2/i18n/strings.g.dart'; import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/shared/constants.dart'; import 'package:moxxyv2/shared/constants.dart';
@ -22,6 +24,7 @@ class ConversationsListRow extends StatefulWidget {
this.showLock = false, this.showLock = false,
this.extra, this.extra,
this.avatarOnTap, this.avatarOnTap,
this.avatarWidget,
super.key, super.key,
} }
); );
@ -31,6 +34,7 @@ class ConversationsListRow extends StatefulWidget {
final bool showLock; final bool showLock;
final bool showTimestamp; final bool showTimestamp;
final void Function()? avatarOnTap; final void Function()? avatarOnTap;
final Widget? avatarWidget;
final Widget? extra; final Widget? extra;
@override @override
@ -83,12 +87,17 @@ class ConversationsListRowState extends State<ConversationsListRow> {
super.dispose(); super.dispose();
} }
Widget _buildAvatar() { Widget _buildAvatar(Uint8List? data) {
final avatar = AvatarWrapper( final avatar = data != null ?
radius: 35, CircleAvatar(
avatarUrl: widget.conversation.avatarUrl, radius: 35,
altText: widget.conversation.title, backgroundImage: MemoryImage(data),
); ) :
AvatarWrapper(
radius: 35,
avatarUrl: widget.conversation.avatarUrl,
altText: widget.conversation.title,
);
if (widget.avatarOnTap != null) { if (widget.avatarOnTap != null) {
return InkWell( return InkWell(
@ -192,8 +201,7 @@ class ConversationsListRowState extends State<ConversationsListRow> {
return const SizedBox(); return const SizedBox();
} }
@override Widget _build(String title, Uint8List? avatar) {
Widget build(BuildContext context) {
final badgeText = widget.conversation.unreadCounter > 99 ? final badgeText = widget.conversation.unreadCounter > 99 ?
'99+' : '99+' :
widget.conversation.unreadCounter.toString(); widget.conversation.unreadCounter.toString();
@ -205,11 +213,12 @@ class ConversationsListRowState extends State<ConversationsListRow> {
final sentBySelf = widget.conversation.lastMessage?.sender == GetIt.I.get<UIDataService>().ownJid!; final sentBySelf = widget.conversation.lastMessage?.sender == GetIt.I.get<UIDataService>().ownJid!;
final showBadge = widget.conversation.unreadCounter > 0 && !sentBySelf; final showBadge = widget.conversation.unreadCounter > 0 && !sentBySelf;
return Padding( return Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Row( child: Row(
children: [ children: [
_buildAvatar(), _buildAvatar(avatar),
Padding( Padding(
padding: const EdgeInsets.only(left: 8), padding: const EdgeInsets.only(left: 8),
child: LimitedBox( child: LimitedBox(
@ -221,7 +230,7 @@ class ConversationsListRowState extends State<ConversationsListRow> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
widget.conversation.title, title,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 17, fontSize: 17,
@ -294,4 +303,27 @@ class ConversationsListRowState extends State<ConversationsListRow> {
), ),
); );
} }
@override
Widget build(BuildContext context) {
if (widget.conversation.contactId != null) {
return FutureBuilder<Contact?>(
future: FlutterContacts.getContact(widget.conversation.contactId!, withThumbnail: true),
builder: (_, snapshot) {
final hasData = snapshot.hasData && snapshot.data != null;
if (hasData) {
return _build(
'${snapshot.data!.name.first} ${snapshot.data!.name.last}',
snapshot.data!.thumbnail,
);
}
return _build(widget.conversation.title, null);
}
);
}
return _build(widget.conversation.title, null);
}
} }

View File

@ -407,6 +407,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.7.0" version: "0.7.0"
flutter_contacts:
dependency: "direct main"
description:
name: flutter_contacts
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.5+1"
flutter_driver: flutter_driver:
dependency: transitive dependency: transitive
description: flutter description: flutter

View File

@ -30,6 +30,7 @@ dependencies:
sdk: flutter sdk: flutter
flutter_bloc: 8.1.1 flutter_bloc: 8.1.1
flutter_blurhash: 0.7.0 flutter_blurhash: 0.7.0
flutter_contacts: 1.1.5+1
flutter_image_compress: 1.1.0 flutter_image_compress: 1.1.0
flutter_isolate: 2.0.2 flutter_isolate: 2.0.2
flutter_localizations: flutter_localizations: