Compare commits

..

3 Commits

Author SHA1 Message Date
10b86812cd fix(service): Fix conversations not being properly loaded
Meanwhile, also remove the Completer system for preventing race
conditions with, hopefully, something better.
2022-11-25 17:58:09 +01:00
fe4c794f68 refactor(service): Conversations now point to the last message 2022-11-25 17:01:30 +01:00
6115d748e3 feat(ui): Show the last message state in the conversations list 2022-11-25 16:09:06 +01:00
22 changed files with 313 additions and 176 deletions

View File

@ -9,6 +9,7 @@ import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/i18n/strings.g.dart'; import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/service/service.dart'; import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/shared/commands.dart'; import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/synchronized_queue.dart';
import 'package:moxxyv2/ui/bloc/addcontact_bloc.dart'; import 'package:moxxyv2/ui/bloc/addcontact_bloc.dart';
import 'package:moxxyv2/ui/bloc/blocklist_bloc.dart'; import 'package:moxxyv2/ui/bloc/blocklist_bloc.dart';
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart'; import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
@ -99,8 +100,6 @@ void setupBlocs(GlobalKey<NavigatorState> navKey) {
// Padding(padding: ..., child: Column(children: [ ... ])) // Padding(padding: ..., child: Column(children: [ ... ]))
// TODO(Unknown): Theme the switches // TODO(Unknown): Theme the switches
void main() async { void main() async {
GetIt.I.registerSingleton<Completer<void>>(Completer());
setupLogging(); setupLogging();
await setupUIServices(); await setupUIServices();
@ -190,10 +189,10 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
// Lift the UI block
GetIt.I.get<Completer<void>>().complete();
_setupSharingHandler(); _setupSharingHandler();
// Lift the UI block
GetIt.I.get<SynchronizedQueue<Map<String, dynamic>?>>().removeQueueLock();
} }
Future<void> _handleSharedMedia(SharedMedia media) async { Future<void> _handleSharedMedia(SharedMedia media) async {

View File

@ -5,6 +5,7 @@ import 'package:moxxyv2/service/database/database.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';
import 'package:moxxyv2/shared/models/message.dart';
class ConversationService { class ConversationService {
ConversationService() ConversationService()
@ -55,10 +56,8 @@ class ConversationService {
/// Wrapper around [DatabaseService]'s [updateConversation] that modifies the cache. /// Wrapper around [DatabaseService]'s [updateConversation] that modifies the cache.
Future<Conversation> updateConversation(int id, { Future<Conversation> updateConversation(int id, {
String? lastMessageBody,
int? lastChangeTimestamp, int? lastChangeTimestamp,
bool? lastMessageRetracted, Message? lastMessage,
int? lastMessageId,
bool? open, bool? open,
int? unreadCounter, int? unreadCounter,
String? avatarUrl, String? avatarUrl,
@ -66,21 +65,24 @@ class ConversationService {
bool? muted, bool? muted,
bool? encrypted, bool? encrypted,
}) async { }) async {
final conversation = await _getConversationById(id); final conversation = (await _getConversationById(id))!;
final newConversation = await GetIt.I.get<DatabaseService>().updateConversation( var newConversation = await GetIt.I.get<DatabaseService>().updateConversation(
id, id,
lastMessageBody: lastMessageBody, lastMessage: lastMessage,
lastMessageRetracted: lastMessageRetracted,
lastMessageId: lastMessageId,
lastChangeTimestamp: lastChangeTimestamp, lastChangeTimestamp: lastChangeTimestamp,
open: open, open: open,
unreadCounter: unreadCounter, unreadCounter: unreadCounter,
avatarUrl: avatarUrl, avatarUrl: avatarUrl,
chatState: conversation?.chatState ?? ChatState.gone, chatState: conversation.chatState,
muted: muted, muted: muted,
encrypted: encrypted, encrypted: encrypted,
); );
// Copy over the old lastMessage if a new one was not set
if (conversation.lastMessage != null && lastMessage == null) {
newConversation = newConversation.copyWith(lastMessage: conversation.lastMessage);
}
_conversationCache.cache(id, newConversation); _conversationCache.cache(id, newConversation);
return newConversation; return newConversation;
} }
@ -88,9 +90,7 @@ class ConversationService {
/// Wrapper around [DatabaseService]'s [addConversationFromData] that updates the cache. /// Wrapper around [DatabaseService]'s [addConversationFromData] that updates the cache.
Future<Conversation> addConversationFromData( Future<Conversation> addConversationFromData(
String title, String title,
int lastMessageId, Message? lastMessage,
bool lastMessageRetracted,
String lastMessageBody,
String avatarUrl, String avatarUrl,
String jid, String jid,
int unreadCounter, int unreadCounter,
@ -101,9 +101,7 @@ class ConversationService {
) async { ) async {
final newConversation = await GetIt.I.get<DatabaseService>().addConversationFromData( final newConversation = await GetIt.I.get<DatabaseService>().addConversationFromData(
title, title,
lastMessageId, lastMessage,
lastMessageRetracted,
lastMessageBody,
avatarUrl, avatarUrl,
jid, jid,
unreadCounter, unreadCounter,

View File

@ -1,5 +1,5 @@
const conversationsTable = 'Conversations'; const conversationsTable = 'Conversations';
const messsagesTable = 'Messages'; const messagesTable = 'Messages';
const rosterTable = 'RosterItems'; const rosterTable = 'RosterItems';
const mediaTable = 'SharedMedia'; const mediaTable = 'SharedMedia';
const preferenceTable = 'Preferences'; const preferenceTable = 'Preferences';

View File

@ -19,7 +19,7 @@ Future<void> createDatabase(Database db, int version) async {
// Messages // Messages
await db.execute( await db.execute(
''' '''
CREATE TABLE $messsagesTable ( CREATE TABLE $messagesTable (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
sender TEXT NOT NULL, sender TEXT NOT NULL,
body TEXT, body TEXT,
@ -52,7 +52,7 @@ Future<void> createDatabase(Database db, int version) async {
isUploading INTEGER NOT NULL, isUploading INTEGER NOT NULL,
mediaSize INTEGER, mediaSize INTEGER,
isRetracted INTEGER, isRetracted INTEGER,
CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messsagesTable (id) CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messagesTable (id)
)''', )''',
); );
@ -66,12 +66,11 @@ Future<void> createDatabase(Database db, int version) async {
avatarUrl TEXT NOT NULL, avatarUrl TEXT NOT NULL,
lastChangeTimestamp INTEGER NOT NULL, lastChangeTimestamp INTEGER NOT NULL,
unreadCounter INTEGER NOT NULL, unreadCounter INTEGER NOT NULL,
lastMessageBody TEXT NOT NULL,
open INTEGER NOT NULL, open INTEGER NOT NULL,
muted INTEGER NOT NULL, muted INTEGER NOT NULL,
encrypted INTEGER NOT NULL, encrypted INTEGER NOT NULL,
lastMessageId INTEGER NOT NULL, lastMessageId INTEGER NOT NULL,
lastMessageRetracted INTEGER NOT NULL, CONSTRAINT fk_last_message FOREIGN KEY (lastMessageId) REFERENCES $messagesTable (id)
)''', )''',
); );
@ -86,7 +85,7 @@ Future<void> createDatabase(Database db, int version) async {
conversation_id INTEGER NOT NULL, conversation_id INTEGER NOT NULL,
message_id INTEGER, message_id INTEGER,
FOREIGN KEY (conversation_id) REFERENCES $conversationsTable (id), FOREIGN KEY (conversation_id) REFERENCES $conversationsTable (id),
FOREIGN KEY (message_id) REFERENCES $messsagesTable (id) FOREIGN KEY (message_id) REFERENCES $messagesTable (id)
)''', )''',
); );

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_conversations.dart';
import 'package:moxxyv2/service/database/migrations/0000_conversations2.dart';
import 'package:moxxyv2/service/database/migrations/0000_language.dart'; import 'package:moxxyv2/service/database/migrations/0000_language.dart';
import 'package:moxxyv2/service/database/migrations/0000_retraction.dart'; import 'package:moxxyv2/service/database/migrations/0000_retraction.dart';
import 'package:moxxyv2/service/database/migrations/0000_retraction_conversation.dart'; import 'package:moxxyv2/service/database/migrations/0000_retraction_conversation.dart';
@ -58,7 +60,7 @@ class DatabaseService {
_db = await openDatabase( _db = await openDatabase(
dbPath, dbPath,
password: key, password: key,
version: 6, version: 8,
onCreate: createDatabase, onCreate: createDatabase,
onConfigure: configureDatabase, onConfigure: configureDatabase,
onUpgrade: (db, oldVersion, newVersion) async { onUpgrade: (db, oldVersion, newVersion) async {
@ -82,6 +84,14 @@ class DatabaseService {
_log.finest('Running migration for database version 6'); _log.finest('Running migration for database version 6');
await upgradeFromV5ToV6(db); await upgradeFromV5ToV6(db);
} }
if (oldVersion < 7) {
_log.finest('Running migration for database version 7');
await upgradeFromV6ToV7(db);
}
if (oldVersion < 8) {
_log.finest('Running migration for database version 8');
await upgradeFromV7ToV8(db);
}
}, },
); );
@ -107,18 +117,22 @@ class DatabaseService {
final rosterItem = await GetIt.I.get<RosterService>() final rosterItem = await GetIt.I.get<RosterService>()
.getRosterItemByJid(c['jid']! as String); .getRosterItemByJid(c['jid']! as String);
Message? lastMessage;
if (c['lastMessageId'] != null) {
lastMessage = await getMessageById(c['lastMessageId']! as int);
}
tmp.add( tmp.add(
Conversation.fromDatabaseJson( Conversation.fromDatabaseJson(
c, c,
rosterItem != null, rosterItem != null,
rosterItem?.subscription ?? 'none', rosterItem?.subscription ?? 'none',
sharedMediaRaw, sharedMediaRaw,
lastMessage,
), ),
); );
} }
_log.finest(tmp.toString());
return tmp; return tmp;
} }
@ -151,10 +165,8 @@ class DatabaseService {
/// Updates the conversation with id [id] inside the database. /// Updates the conversation with id [id] inside the database.
Future<Conversation> updateConversation(int id, { Future<Conversation> updateConversation(int id, {
String? lastMessageBody,
int? lastChangeTimestamp, int? lastChangeTimestamp,
bool? lastMessageRetracted, Message? lastMessage,
int? lastMessageId,
bool? open, bool? open,
int? unreadCounter, int? unreadCounter,
String? avatarUrl, String? avatarUrl,
@ -176,15 +188,8 @@ class DatabaseService {
orderBy: 'timestamp DESC', orderBy: 'timestamp DESC',
)).map(SharedMedium.fromDatabaseJson); )).map(SharedMedium.fromDatabaseJson);
//await c.sharedMedia.load(); if (lastMessage != null) {
if (lastMessageBody != null) { c['lastMessageId'] = lastMessage.id;
c['lastMessageBody'] = lastMessageBody;
}
if (lastMessageRetracted != null) {
c['lastMessageRetracted'] = boolToInt(lastMessageRetracted);
}
if (lastMessageId != null) {
c['lastMessageId'] = lastMessageId;
} }
if (lastChangeTimestamp != null) { if (lastChangeTimestamp != null) {
c['lastChangeTimestamp'] = lastChangeTimestamp; c['lastChangeTimestamp'] = lastChangeTimestamp;
@ -218,6 +223,7 @@ class DatabaseService {
rosterItem != null, rosterItem != null,
rosterItem?.subscription ?? 'none', rosterItem?.subscription ?? 'none',
sharedMedia.map((m) => m.toJson()).toList(), sharedMedia.map((m) => m.toJson()).toList(),
lastMessage,
); );
} }
@ -225,9 +231,7 @@ class DatabaseService {
/// [Conversation] object can carry its database id. /// [Conversation] object can carry its database id.
Future<Conversation> addConversationFromData( Future<Conversation> addConversationFromData(
String title, String title,
int lastMessageId, Message? lastMessage,
bool lastMessageRetracted,
String lastMessageBody,
String avatarUrl, String avatarUrl,
String jid, String jid,
int unreadCounter, int unreadCounter,
@ -239,9 +243,7 @@ class DatabaseService {
final rosterItem = await GetIt.I.get<RosterService>().getRosterItemByJid(jid); final rosterItem = await GetIt.I.get<RosterService>().getRosterItemByJid(jid);
final conversation = Conversation( final conversation = Conversation(
title, title,
lastMessageId, lastMessage,
lastMessageRetracted,
lastMessageBody,
avatarUrl, avatarUrl,
jid, jid,
unreadCounter, unreadCounter,

View File

@ -0,0 +1,13 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/shared/models/conversation.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV6ToV7(Database db) async {
await db.execute(
'ALTER TABLE $conversationsTable ADD COLUMN lastMessageState INTEGER NOT NULL DEFAULT 0;'
);
await db.execute(
"ALTER TABLE $conversationsTable ADD COLUMN lastMessageSender TEXT NOT NULL DEFAULT '';"
);
}

View File

@ -0,0 +1,18 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV7ToV8(Database db) async {
// TODO(PapaTutuWawa): Make lastMessageId a foreign key constraint
await db.execute(
'ALTER TABLE $conversationsTable DROP COLUMN lastMessageState;'
);
await db.execute(
"ALTER TABLE $conversationsTable DROP COLUMN lastMessageSender;"
);
await db.execute(
"ALTER TABLE $conversationsTable DROP COLUMN lastMessageBody;"
);
await db.execute(
"ALTER TABLE $conversationsTable DROP COLUMN lastMessageRetracted;"
);
}

View File

@ -6,6 +6,6 @@ import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV3ToV4(Database db) async { Future<void> upgradeFromV3ToV4(Database db) async {
// Mark all messages as not retracted // Mark all messages as not retracted
await db.execute( await db.execute(
'ALTER TABLE $messsagesTable ADD COLUMN isRetracted INTEGER DEFAULT ${boolToInt(false)};', 'ALTER TABLE $messagesTable ADD COLUMN isRetracted INTEGER DEFAULT ${boolToInt(false)};',
); );
} }

View File

@ -5,6 +5,6 @@ import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV5ToV6(Database db) async { Future<void> upgradeFromV5ToV6(Database db) async {
// Allow shared media to reference a message // Allow shared media to reference a message
await db.execute( await db.execute(
'ALTER TABLE $mediaTable ADD COLUMN message_id INTEGER REFERENCES $messsagesTable (id);', 'ALTER TABLE $mediaTable ADD COLUMN message_id INTEGER REFERENCES $messagesTable (id);',
); );
} }

View File

@ -29,6 +29,7 @@ import 'package:moxxyv2/shared/eventhandler.dart';
import 'package:moxxyv2/shared/events.dart'; import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/shared/helpers.dart'; import 'package:moxxyv2/shared/helpers.dart';
import 'package:moxxyv2/shared/models/preferences.dart'; import 'package:moxxyv2/shared/models/preferences.dart';
import 'package:moxxyv2/shared/synchronized_queue.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
void setupBackgroundEventHandler() { void setupBackgroundEventHandler() {
@ -68,6 +69,9 @@ void setupBackgroundEventHandler() {
]); ]);
GetIt.I.registerSingleton<EventHandler>(handler); GetIt.I.registerSingleton<EventHandler>(handler);
GetIt.I.registerSingleton<SynchronizedQueue<Map<String, dynamic>?>>(
SynchronizedQueue<Map<String, dynamic>?>(handleUIEvent),
);
} }
Future<void> performLogin(LoginCommand command, { dynamic extra }) async { Future<void> performLogin(LoginCommand command, { dynamic extra }) async {
@ -141,13 +145,6 @@ Future<PreStartDoneEvent> _buildPreStartDoneEvent(PreferencesState preferences)
Future<void> performPreStart(PerformPreStartCommand command, { dynamic extra }) async { Future<void> performPreStart(PerformPreStartCommand command, { dynamic extra }) async {
final id = extra as String; final id = extra as String;
// Prevent a race condition where the UI sends the prestart command before the service
// has finished setting everything up
GetIt.I.get<Logger>().finest('Waiting for preStart future to complete..');
await GetIt.I.get<Completer<void>>().future;
GetIt.I.get<Logger>().finest('PreStart future done');
final preferences = await GetIt.I.get<PreferencesService>().getPreferences(); final preferences = await GetIt.I.get<PreferencesService>().getPreferences();
// Set the locale very early // Set the locale very early
@ -209,9 +206,7 @@ Future<void> performAddConversation(AddConversationCommand command, { dynamic ex
} else { } else {
final conversation = await cs.addConversationFromData( final conversation = await cs.addConversationFromData(
command.title, command.title,
-1, null,
false,
command.lastMessageBody,
command.avatarUrl, command.avatarUrl,
command.jid, command.jid,
0, 0,
@ -335,9 +330,7 @@ Future<void> performAddContact(AddContactCommand command, { dynamic extra }) asy
} else { } else {
final c = await cs.addConversationFromData( final c = await cs.addConversationFromData(
jid.split('@')[0], jid.split('@')[0],
-1, null,
false,
'',
'', '',
jid, jid,
0, 0,
@ -621,14 +614,10 @@ Future<void> performMarkConversationAsRead(MarkConversationAsReadCommand command
sendEvent(ConversationUpdatedEvent(conversation: conversation)); sendEvent(ConversationUpdatedEvent(conversation: conversation));
final msg = await GetIt.I.get<MessageService>().getMessageById( if (conversation.lastMessage != null) {
conversation.jid,
conversation.lastMessageId,
);
if (msg != null) {
await GetIt.I.get<XmppService>().sendReadMarker( await GetIt.I.get<XmppService>().sendReadMarker(
conversation.jid, conversation.jid,
msg.sid, conversation.lastMessage!.sid,
); );
} }
} }

View File

@ -239,17 +239,16 @@ class MessageService {
mediaSize: null, mediaSize: null,
isRetracted: true, isRetracted: true,
thumbnailData: null, thumbnailData: null,
body: '',
); );
sendEvent(MessageUpdatedEvent(message: retractedMessage)); sendEvent(MessageUpdatedEvent(message: retractedMessage));
final cs = GetIt.I.get<ConversationService>(); final cs = GetIt.I.get<ConversationService>();
final conversation = await cs.getConversationByJid(conversationJid); final conversation = await cs.getConversationByJid(conversationJid);
if (conversation != null) { if (conversation != null) {
if (conversation.lastMessageId == msg.id) { if (conversation.lastMessage?.id == msg.id) {
var newConversation = await cs.updateConversation( var newConversation = conversation.copyWith(
conversation.id, lastMessage: retractedMessage,
lastMessageBody: '',
lastMessageRetracted: true,
); );
if (isMedia) { if (isMedia) {
@ -260,7 +259,6 @@ class MessageService {
return medium.messageId != msg.id; return medium.messageId != msg.id;
}).toList(), }).toList(),
); );
GetIt.I.get<ConversationService>().setConversation(newConversation);
// Delete the file if we downloaded it // Delete the file if we downloaded it
if (mediaUrl != null) { if (mediaUrl != null) {
@ -271,6 +269,7 @@ class MessageService {
} }
} }
cs.setConversation(newConversation);
sendEvent( sendEvent(
ConversationUpdatedEvent( ConversationUpdatedEvent(
conversation: newConversation, conversation: newConversation,

View File

@ -35,6 +35,7 @@ import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/eventhandler.dart'; import 'package:moxxyv2/shared/eventhandler.dart';
import 'package:moxxyv2/shared/events.dart'; import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/shared/logging.dart'; import 'package:moxxyv2/shared/logging.dart';
import 'package:moxxyv2/shared/synchronized_queue.dart';
import 'package:moxxyv2/ui/events.dart' as ui_events; import 'package:moxxyv2/ui/events.dart' as ui_events;
Future<void> initializeServiceIfNeeded() async { Future<void> initializeServiceIfNeeded() async {
@ -47,7 +48,7 @@ Future<void> initializeServiceIfNeeded() async {
} }
logger.info('Attaching to service...'); logger.info('Attaching to service...');
await handler.attach(ui_events.handleIsolateEvent); await handler.attach(ui_events.receiveIsolateEvent);
logger.info('Done'); logger.info('Done');
// ignore: cascade_invocations // ignore: cascade_invocations
@ -62,7 +63,7 @@ Future<void> initializeServiceIfNeeded() async {
logger.info('Service is not running. Initializing service... '); logger.info('Service is not running. Initializing service... ');
await handler.start( await handler.start(
entrypoint, entrypoint,
handleUiEvent, receiveUIEvent,
ui_events.handleIsolateEvent, ui_events.handleIsolateEvent,
); );
} }
@ -72,7 +73,7 @@ Future<void> initializeServiceIfNeeded() async {
/// logging what we send. /// logging what we send.
void sendEvent(BackgroundEvent event, { String? id }) { void sendEvent(BackgroundEvent event, { String? id }) {
// NOTE: *S*erver to *F*oreground // NOTE: *S*erver to *F*oreground
GetIt.I.get<Logger>().fine('S2F: ${event.toJson()}'); GetIt.I.get<Logger>().fine('--> ${event.toJson()["type"]}');
GetIt.I.get<BackgroundService>().sendEvent(event, id: id); GetIt.I.get<BackgroundService>().sendEvent(event, id: id);
} }
@ -130,17 +131,14 @@ Future<void> initUDPLogger() async {
/// The entrypoint for all platforms after the platform specific initilization is done. /// The entrypoint for all platforms after the platform specific initilization is done.
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> entrypoint() async { Future<void> entrypoint() async {
// Register the lock setupLogging();
GetIt.I.registerSingleton<Completer<void>>(Completer()); setupBackgroundEventHandler();
// Register singletons // Register singletons
GetIt.I.registerSingleton<Logger>(Logger('MoxxyService')); GetIt.I.registerSingleton<Logger>(Logger('MoxxyService'));
GetIt.I.registerSingleton<UDPLogger>(UDPLogger()); GetIt.I.registerSingleton<UDPLogger>(UDPLogger());
GetIt.I.registerSingleton<LanguageService>(LanguageService()); GetIt.I.registerSingleton<LanguageService>(LanguageService());
setupLogging();
setupBackgroundEventHandler();
// Initialize the database // Initialize the database
GetIt.I.registerSingleton<DatabaseService>(DatabaseService()); GetIt.I.registerSingleton<DatabaseService>(DatabaseService());
await GetIt.I.get<DatabaseService>().initialize(); await GetIt.I.get<DatabaseService>().initialize();
@ -242,13 +240,15 @@ Future<void> entrypoint() async {
); );
} }
GetIt.I.get<Logger>().finest('Resolving startup future'); unawaited(GetIt.I.get<SynchronizedQueue<Map<String, dynamic>?>>().removeQueueLock());
GetIt.I.get<Completer<void>>().complete();
sendEvent(ServiceReadyEvent()); sendEvent(ServiceReadyEvent());
} }
Future<void> handleUiEvent(Map<String, dynamic>? data) async { Future<void> receiveUIEvent(Map<String, dynamic>? data) async {
await GetIt.I.get<SynchronizedQueue<Map<String, dynamic>?>>().add(data);
}
Future<void> handleUIEvent(Map<String, dynamic>? data) async {
// NOTE: *F*oreground to *S*ervice // NOTE: *F*oreground to *S*ervice
final log = GetIt.I.get<Logger>(); final log = GetIt.I.get<Logger>();

View File

@ -158,9 +158,7 @@ class XmppService {
); );
final newConversation = await cs.updateConversation( final newConversation = await cs.updateConversation(
conversation.id, conversation.id,
lastMessageBody: body, lastMessage: message,
lastMessageId: message.id,
lastMessageRetracted: false,
lastChangeTimestamp: timestamp, lastChangeTimestamp: timestamp,
); );
@ -364,7 +362,7 @@ class XmppService {
// Recipient -> Should encrypt // Recipient -> Should encrypt
final encrypt = <String, bool>{}; final encrypt = <String, bool>{};
// Recipient -> Last message Id // Recipient -> Last message Id
final lastMessageIds = <String, int>{}; final lastMessages = <String, Message>{};
// Create the messages and shared media entries // Create the messages and shared media entries
final conn = GetIt.I.get<XmppConnection>(); final conn = GetIt.I.get<XmppConnection>();
@ -412,7 +410,7 @@ class XmppService {
} }
if (path == paths.last) { if (path == paths.last) {
lastMessageIds[recipient] = msg.id; lastMessages[recipient] = msg;
} }
sendEvent(MessageAddedEvent(message: msg)); sendEvent(MessageAddedEvent(message: msg));
@ -424,14 +422,12 @@ class XmppService {
final sharedMediaMap = <String, List<SharedMedium>>{}; final sharedMediaMap = <String, List<SharedMedium>>{};
final rs = GetIt.I.get<RosterService>(); final rs = GetIt.I.get<RosterService>();
for (final recipient in recipients) { for (final recipient in recipients) {
final lastFileMime = lookupMimeType(paths.last);
final conversation = await cs.getConversationByJid(recipient); final conversation = await cs.getConversationByJid(recipient);
if (conversation != null) { if (conversation != null) {
// Update conversation // Update conversation
var updatedConversation = await cs.updateConversation( var updatedConversation = await cs.updateConversation(
conversation.id, conversation.id,
lastMessageBody: mimeTypeToEmoji(lastFileMime), lastMessage: lastMessages[recipient],
lastMessageId: lastMessageIds[recipient],
lastChangeTimestamp: DateTime.now().millisecondsSinceEpoch, lastChangeTimestamp: DateTime.now().millisecondsSinceEpoch,
open: true, open: true,
); );
@ -452,9 +448,7 @@ class XmppService {
var newConversation = await cs.addConversationFromData( var newConversation = await cs.addConversationFromData(
// TODO(Unknown): Should we use the JID parser? // TODO(Unknown): Should we use the JID parser?
rosterItem?.title ?? recipient.split('@').first, rosterItem?.title ?? recipient.split('@').first,
lastMessageIds[recipient]!, lastMessages[recipient],
false,
mimeTypeToEmoji(lastFileMime),
rosterItem?.avatarUrl ?? '', rosterItem?.avatarUrl ?? '',
recipient, recipient,
0, 0,
@ -653,9 +647,7 @@ class XmppService {
final bare = event.from.toBare(); final bare = event.from.toBare();
final conv = await cs.addConversationFromData( final conv = await cs.addConversationFromData(
bare.toString().split('@')[0], bare.toString().split('@')[0],
-1, null,
false,
'',
'', // TODO(Unknown): avatarUrl '', // TODO(Unknown): avatarUrl
bare.toString(), bare.toString(),
0, 0,
@ -693,7 +685,9 @@ class XmppService {
final db = GetIt.I.get<DatabaseService>(); final db = GetIt.I.get<DatabaseService>();
final ms = GetIt.I.get<MessageService>(); final ms = GetIt.I.get<MessageService>();
final dbMsg = await db.getMessageByXmppId(event.id, event.from.toBare().toString()); final cs = GetIt.I.get<ConversationService>();
final sender = event.from.toBare().toString();
final dbMsg = await db.getMessageByXmppId(event.id, sender);
if (dbMsg == null) { if (dbMsg == null) {
_log.warning('Did not find the message in the database!'); _log.warning('Did not find the message in the database!');
return; return;
@ -704,8 +698,15 @@ class XmppService {
received: dbMsg.received || event.type == 'received' || event.type == 'displayed', received: dbMsg.received || event.type == 'received' || event.type == 'displayed',
displayed: dbMsg.displayed || event.type == 'displayed', displayed: dbMsg.displayed || event.type == 'displayed',
); );
sendEvent(MessageUpdatedEvent(message: msg)); sendEvent(MessageUpdatedEvent(message: msg));
// Update the conversation
final conv = await cs.getConversationByJid(sender);
if (conv != null && conv.lastMessage?.id == msg.id) {
final newConv = conv.copyWith(lastMessage: msg);
cs.setConversation(newConv);
sendEvent(ConversationUpdatedEvent(conversation: newConv));
}
} }
Future<void> _onChatState(ChatState state, String jid) async { Future<void> _onChatState(ChatState state, String jid) async {
@ -989,10 +990,8 @@ class XmppService {
// The conversation exists, so we can just update it // The conversation exists, so we can just update it
final newConversation = await cs.updateConversation( final newConversation = await cs.updateConversation(
conversation.id, conversation.id,
lastMessageBody: conversationBody, lastMessage: message,
lastChangeTimestamp: messageTimestamp, lastChangeTimestamp: messageTimestamp,
lastMessageId: message.id,
lastMessageRetracted: false,
// Do not increment the counter for messages we sent ourselves (via Carbons) // Do not increment the counter for messages we sent ourselves (via Carbons)
// or if we have the chat currently opened // or if we have the chat currently opened
unreadCounter: isConversationOpened || sent unreadCounter: isConversationOpened || sent
@ -1017,9 +1016,7 @@ class XmppService {
// The conversation does not exist, so we must create it // The conversation does not exist, so we must create it
final newConversation = await cs.addConversationFromData( final newConversation = await cs.addConversationFromData(
rosterItem?.title ?? conversationJid.split('@')[0], rosterItem?.title ?? conversationJid.split('@')[0],
message.id, message,
false,
conversationBody,
rosterItem?.avatarUrl ?? '', rosterItem?.avatarUrl ?? '',
conversationJid, conversationJid,
sent ? 0 : 1, sent ? 0 : 1,
@ -1128,11 +1125,20 @@ class XmppService {
Future<void> _onStanzaAcked(StanzaAckedEvent event, { dynamic extra }) async { Future<void> _onStanzaAcked(StanzaAckedEvent event, { dynamic extra }) async {
final jid = JID.fromString(event.stanza.to!).toBare().toString(); final jid = JID.fromString(event.stanza.to!).toBare().toString();
final ms = GetIt.I.get<MessageService>(); final ms = GetIt.I.get<MessageService>();
final cs = GetIt.I.get<ConversationService>();
final msg = await ms.getMessageByStanzaId(jid, event.stanza.id!); final msg = await ms.getMessageByStanzaId(jid, event.stanza.id!);
if (msg != null) { if (msg != null) {
// Ack the message
final newMsg = await ms.updateMessage(msg.id, acked: true); final newMsg = await ms.updateMessage(msg.id, acked: true);
sendEvent(MessageUpdatedEvent(message: newMsg)); sendEvent(MessageUpdatedEvent(message: newMsg));
// Ack the conversation
final conv = await cs.getConversationByJid(jid);
if (conv != null && conv.lastMessage?.id == newMsg.id) {
final newConv = conv.copyWith(lastMessage: msg);
cs.setConversation(newConv);
sendEvent(ConversationUpdatedEvent(conversation: newConv));
}
} else { } else {
_log.finest('Wanted to mark message as acked but did not find the message to ack'); _log.finest('Wanted to mark message as acked but did not find the message to ack');
} }

View File

@ -2,6 +2,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/database/helpers.dart'; import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/shared/models/media.dart'; import 'package:moxxyv2/shared/models/media.dart';
import 'package:moxxyv2/shared/models/message.dart';
part 'conversation.freezed.dart'; part 'conversation.freezed.dart';
part 'conversation.g.dart'; part 'conversation.g.dart';
@ -18,14 +19,27 @@ class ConversationChatStateConverter implements JsonConverter<ChatState, Map<Str
}; };
} }
class ConversationMessageConverter implements JsonConverter<Message?, Map<String, dynamic>> {
const ConversationMessageConverter();
@override
Message? fromJson(Map<String, dynamic> json) {
if (json['message'] == null) return null;
return Message.fromJson(json['message']! as Map<String, dynamic>);
}
@override
Map<String, dynamic> toJson(Message? message) => <String, dynamic>{
'message': message?.toJson(),
};
}
@freezed @freezed
class Conversation with _$Conversation { class Conversation with _$Conversation {
factory Conversation( factory Conversation(
String title, String title,
// NOTE: The internal database Id of the message @ConversationMessageConverter() Message? lastMessage,
int lastMessageId,
bool lastMessageRetracted,
String lastMessageBody,
String avatarUrl, String avatarUrl,
String jid, String jid,
int unreadCounter, int unreadCounter,
@ -52,7 +66,7 @@ class Conversation with _$Conversation {
/// JSON /// JSON
factory Conversation.fromJson(Map<String, dynamic> json) => _$ConversationFromJson(json); factory Conversation.fromJson(Map<String, dynamic> json) => _$ConversationFromJson(json);
factory Conversation.fromDatabaseJson(Map<String, dynamic> json, bool inRoster, String subscription, List<Map<String, dynamic>> sharedMedia) { factory Conversation.fromDatabaseJson(Map<String, dynamic> json, bool inRoster, String subscription, List<Map<String, dynamic>> sharedMedia, Message? lastMessage) {
return Conversation.fromJson({ return Conversation.fromJson({
...json, ...json,
'muted': intToBool(json['muted']! as int), 'muted': intToBool(json['muted']! as int),
@ -62,7 +76,9 @@ class Conversation with _$Conversation {
'subscription': subscription, 'subscription': subscription,
'encrypted': intToBool(json['encrypted']! as int), 'encrypted': intToBool(json['encrypted']! as int),
'chatState': const ConversationChatStateConverter().toJson(ChatState.gone), 'chatState': const ConversationChatStateConverter().toJson(ChatState.gone),
'lastMessageRetracted': intToBool(json['lastMessageRetracted']! as int) 'lastMessage': <String, dynamic>{
'message': lastMessage?.toJson(),
},
}); });
} }
@ -72,14 +88,15 @@ class Conversation with _$Conversation {
..remove('chatState') ..remove('chatState')
..remove('sharedMedia') ..remove('sharedMedia')
..remove('inRoster') ..remove('inRoster')
..remove('subscription'); ..remove('subscription')
..remove('lastMessage');
return { return {
...map, ...map,
'open': boolToInt(open), 'open': boolToInt(open),
'muted': boolToInt(muted), 'muted': boolToInt(muted),
'encrypted': boolToInt(encrypted), 'encrypted': boolToInt(encrypted),
'lastMessageRetracted': boolToInt(lastMessageRetracted), 'lastMessage': lastMessage?.id,
}; };
} }
} }

View File

@ -0,0 +1,43 @@
import 'dart:async';
import 'dart:collection';
import 'package:synchronized/synchronized.dart';
/// The function of this class is essentially a queue, that processes itself as long as
/// _shouldQueue is false. If not, all added items are held until removeQueueLock is
/// called. After that point, all added items bypass the lock and get immediately passed
/// to the callback.
class SynchronizedQueue<T> {
SynchronizedQueue(this._callback);
final Future<void> Function(T) _callback;
final Queue<T> _queue = Queue<T>();
final Lock _lock = Lock();
// If true, then events queue up
bool _shouldQueue = true;
Future<void> add(T item) async {
if (!_shouldQueue) {
unawaited(_callback(item));
return;
}
await _lock.synchronized(() {
if (!_shouldQueue) {
unawaited(_callback(item));
return;
}
_queue.addLast(item);
});
}
Future<void> removeQueueLock() async {
await _lock.synchronized(() async {
while (_queue.isNotEmpty) {
final item = _queue.removeFirst();
await _callback(item);
}
_shouldQueue = false;
});
}
}

View File

@ -8,6 +8,7 @@ import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/commands.dart'; import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/eventhandler.dart'; import 'package:moxxyv2/shared/eventhandler.dart';
import 'package:moxxyv2/shared/events.dart'; import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/shared/synchronized_queue.dart';
import 'package:moxxyv2/ui/bloc/blocklist_bloc.dart' as blocklist; import 'package:moxxyv2/ui/bloc/blocklist_bloc.dart' as blocklist;
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart' as conversation; import 'package:moxxyv2/ui/bloc/conversation_bloc.dart' as conversation;
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart' as conversations; import 'package:moxxyv2/ui/bloc/conversations_bloc.dart' as conversations;
@ -34,6 +35,11 @@ void setupEventHandler() {
]); ]);
GetIt.I.registerSingleton<EventHandler>(handler); GetIt.I.registerSingleton<EventHandler>(handler);
GetIt.I.registerSingleton<SynchronizedQueue<Map<String, dynamic>?>>(SynchronizedQueue<Map<String, dynamic>?>(handleIsolateEvent));
}
Future<void> receiveIsolateEvent(Map<String, dynamic>? json) async {
await GetIt.I.get<SynchronizedQueue<Map<String, dynamic>?>>().add(json);
} }
Future<void> handleIsolateEvent(Map<String, dynamic>? json) async { Future<void> handleIsolateEvent(Map<String, dynamic>? json) async {
@ -50,7 +56,7 @@ Future<void> handleIsolateEvent(Map<String, dynamic>? json) async {
event, event,
); );
log.finest('S2F: $event'); log.finest('<-- ${(event).toString()}');
// First attempt to deal with awaitables // First attempt to deal with awaitables
var found = false; var found = false;
@ -136,9 +142,6 @@ Future<void> onSelfAvatarChanged(SelfAvatarChangedEvent event, { dynamic extra }
} }
Future<void> onServiceReady(ServiceReadyEvent event, { dynamic extra }) async { Future<void> onServiceReady(ServiceReadyEvent event, { dynamic extra }) async {
GetIt.I.get<Logger>().fine('onServiceReady: Waiting for UI future to resolve...');
await GetIt.I.get<Completer<void>>().future;
GetIt.I.get<Logger>().fine('onServiceReady: Done');
await MoxplatformPlugin.handler.getDataSender().sendData( await MoxplatformPlugin.handler.getDataSender().sendData(
PerformPreStartCommand( PerformPreStartCommand(
systemLocaleCode: WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag(), systemLocaleCode: WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag(),

View File

@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/i18n/strings.g.dart'; import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart'; import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart'; import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
@ -55,15 +54,9 @@ class ConversationsPageState extends State<ConversationsPage> with TickerProvide
itemBuilder: (_context, index) { itemBuilder: (_context, index) {
final item = state.conversations[index]; final item = state.conversations[index];
final row = ConversationsListRow( final row = ConversationsListRow(
item.avatarUrl,
item.title,
item.lastMessageBody,
item.unreadCounter,
maxTextWidth, maxTextWidth,
item.lastChangeTimestamp, item,
true, true,
typingIndicator: item.chatState == ChatState.composing,
lastMessageRetracted: item.lastMessageRetracted,
key: ValueKey('conversationRow;${item.jid}'), key: ValueKey('conversationRow;${item.jid}'),
); );

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:moxxmpp/moxxmpp.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/models/conversation.dart';
import 'package:moxxyv2/ui/bloc/newconversation_bloc.dart'; import 'package:moxxyv2/ui/bloc/newconversation_bloc.dart';
import 'package:moxxyv2/ui/constants.dart'; import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/helpers.dart'; import 'package:moxxyv2/ui/helpers.dart';
@ -94,7 +95,27 @@ class NewConversationPage extends StatelessWidget {
item.avatarUrl, item.avatarUrl,
), ),
), ),
child: ConversationsListRow(item.avatarUrl, item.title, item.jid, 0, maxTextWidth, timestampNever, false), child: ConversationsListRow(
maxTextWidth,
Conversation(
item.title,
null,
item.avatarUrl,
item.jid,
0,
0,
[],
0,
true,
true,
'',
false,
false,
ChatState.gone,
),
false,
showTimestamp: false,
),
), ),
); );
} }

View File

@ -2,8 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:move_to_background/move_to_background.dart'; import 'package:move_to_background/move_to_background.dart';
import 'package:moxxmpp/moxxmpp.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/models/conversation.dart';
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart' as navigation; import 'package:moxxyv2/ui/bloc/navigation_bloc.dart' as navigation;
import 'package:moxxyv2/ui/bloc/share_selection_bloc.dart'; import 'package:moxxyv2/ui/bloc/share_selection_bloc.dart';
import 'package:moxxyv2/ui/constants.dart'; import 'package:moxxyv2/ui/constants.dart';
@ -68,14 +69,26 @@ class ShareSelectionPage extends StatelessWidget {
); );
}, },
child: ConversationsListRow( child: ConversationsListRow(
item.avatarPath, maxTextWidth,
Conversation(
item.title, item.title,
null,
item.avatarPath,
item.jid, item.jid,
0, 0,
maxTextWidth, 0,
timestampNever, [],
0,
true,
true,
'',
false,
false,
ChatState.gone,
),
false, false,
showLock: item.isEncrypted, showLock: item.isEncrypted,
showTimestamp: false,
extra: Checkbox( extra: Checkbox(
value: isSelected, value: isSelected,
onChanged: (_) { onChanged: (_) {

View File

@ -15,10 +15,6 @@ import 'package:share_handler/share_handler.dart';
/// Handler for when we received a [PreStartDoneEvent]. /// Handler for when we received a [PreStartDoneEvent].
Future<void> preStartDone(PreStartDoneEvent result, { dynamic extra }) async { Future<void> preStartDone(PreStartDoneEvent result, { dynamic extra }) async {
GetIt.I.get<Logger>().finest('Waiting for UI setup future to complete...');
await GetIt.I.get<Completer<void>>().future;
GetIt.I.get<Logger>().finest('Done');
GetIt.I.get<PreferencesBloc>().add( GetIt.I.get<PreferencesBloc>().add(
PreferencesChangedEvent(result.preferences), PreferencesChangedEvent(result.preferences),
); );

View File

@ -5,7 +5,6 @@ import 'package:moxxyv2/ui/bloc/conversation_bloc.dart' as conversation;
// TODO(Unknown): Maybe manage this as a sort-of proxy service between the BLoCs and // TODO(Unknown): Maybe manage this as a sort-of proxy service between the BLoCs and
// the event receiver // the event receiver
class UIDataService { class UIDataService {
UIDataService() : isLoggedIn = false; UIDataService() : isLoggedIn = false;
bool isLoggedIn; bool isLoggedIn;

View File

@ -1,39 +1,33 @@
import 'dart:async'; import 'dart:async';
import 'package:badges/badges.dart'; import 'package:badges/badges.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:moxxmpp/moxxmpp.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';
import 'package:moxxyv2/shared/helpers.dart'; import 'package:moxxyv2/shared/helpers.dart';
import 'package:moxxyv2/shared/models/conversation.dart';
import 'package:moxxyv2/ui/constants.dart'; import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/service/data.dart';
import 'package:moxxyv2/ui/widgets/avatar.dart'; import 'package:moxxyv2/ui/widgets/avatar.dart';
import 'package:moxxyv2/ui/widgets/chat/typing.dart'; import 'package:moxxyv2/ui/widgets/chat/typing.dart';
class ConversationsListRow extends StatefulWidget { class ConversationsListRow extends StatefulWidget {
const ConversationsListRow( const ConversationsListRow(
this.avatarUrl,
this.name,
this.lastMessageBody,
this.unreadCount,
this.maxTextWidth, this.maxTextWidth,
this.lastChangeTimestamp, this.conversation,
this.update, { this.update, {
this.showTimestamp = true,
this.showLock = false, this.showLock = false,
this.typingIndicator = false,
this.extra, this.extra,
this.lastMessageRetracted = false,
super.key, super.key,
} }
); );
final String avatarUrl; final Conversation conversation;
final String name;
final bool lastMessageRetracted;
final String lastMessageBody;
final int unreadCount;
final double maxTextWidth; final double maxTextWidth;
final int lastChangeTimestamp;
final bool update; // Should a timer run to update the timestamp final bool update; // Should a timer run to update the timestamp
final bool typingIndicator;
final bool showLock; final bool showLock;
final bool showTimestamp;
final Widget? extra; final Widget? extra;
@override @override
@ -51,23 +45,23 @@ class ConversationsListRowState extends State<ConversationsListRow> {
final _now = DateTime.now().millisecondsSinceEpoch; final _now = DateTime.now().millisecondsSinceEpoch;
_timestampString = formatConversationTimestamp( _timestampString = formatConversationTimestamp(
widget.lastChangeTimestamp, widget.conversation.lastChangeTimestamp,
_now, _now,
); );
// NOTE: We could also check and run the timer hourly, but who has a messenger on the // NOTE: We could also check and run the timer hourly, but who has a messenger on the
// conversation screen open for hours on end? // conversation screen open for hours on end?
if (widget.update && widget.lastChangeTimestamp > -1 && _now - widget.lastChangeTimestamp >= 60 * Duration.millisecondsPerMinute) { if (widget.update && widget.conversation.lastChangeTimestamp > -1 && _now - widget.conversation.lastChangeTimestamp >= 60 * Duration.millisecondsPerMinute) {
_updateTimer = Timer.periodic(const Duration(minutes: 1), (timer) { _updateTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
final now = DateTime.now().millisecondsSinceEpoch; final now = DateTime.now().millisecondsSinceEpoch;
setState(() { setState(() {
_timestampString = formatConversationTimestamp( _timestampString = formatConversationTimestamp(
widget.lastChangeTimestamp, widget.conversation.lastChangeTimestamp,
now, now,
); );
}); });
if (now - widget.lastChangeTimestamp >= 60 * Duration.millisecondsPerMinute) { if (now - widget.conversation.lastChangeTimestamp >= 60 * Duration.millisecondsPerMinute) {
_updateTimer!.cancel(); _updateTimer!.cancel();
_updateTimer = null; _updateTimer = null;
} }
@ -87,38 +81,69 @@ class ConversationsListRowState extends State<ConversationsListRow> {
} }
Widget _buildLastMessageBody() { Widget _buildLastMessageBody() {
if (widget.typingIndicator) { if (widget.conversation.chatState == ChatState.composing) {
return const TypingIndicatorWidget(Colors.black, Colors.white); return const TypingIndicatorWidget(Colors.black, Colors.white);
} }
String body;
if (widget.conversation.lastMessage == null) {
body = '';
} else {
if (widget.conversation.lastMessage!.isRetracted) {
body = t.messages.retracted;
} else {
body = widget.conversation.lastMessage!.body;
}
}
return Text( return Text(
widget.lastMessageRetracted ? body,
t.messages.retracted :
widget.lastMessageBody,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
); );
} }
Widget _getLastMessageIcon() {
final lastMessage = widget.conversation.lastMessage;
if (lastMessage == null) return const SizedBox();
if (lastMessage.displayed) {
return Icon(
Icons.done_all,
color: Colors.blue.shade700,
);
} else if (lastMessage.received) {
return const Icon(Icons.done_all);
} else if (lastMessage.acked) {
return const Icon(Icons.done);
}
return const SizedBox();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final badgeText = widget.unreadCount > 99 ? '99+' : widget.unreadCount.toString(); final badgeText = widget.conversation.unreadCounter > 99 ?
'99+' :
widget.conversation.unreadCounter.toString();
// TODO(Unknown): Maybe turn this into an attribute of the widget to prevent calling this // TODO(Unknown): Maybe turn this into an attribute of the widget to prevent calling this
// for every conversation // for every conversation
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final width = screenWidth - 24 - 70; final width = screenWidth - 24 - 70;
final textWidth = screenWidth * 0.6; final textWidth = screenWidth * 0.6;
final showTimestamp = widget.lastChangeTimestamp != timestampNever; final showTimestamp = widget.conversation.lastChangeTimestamp != timestampNever && widget.showTimestamp;
final showBadge = widget.unreadCount > 0; final sentBySelf = widget.conversation.lastMessage?.sender == GetIt.I.get<UIDataService>().ownJid!;
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: [
AvatarWrapper( AvatarWrapper(
radius: 35, radius: 35,
avatarUrl: widget.avatarUrl, avatarUrl: widget.conversation.avatarUrl,
altText: widget.name, altText: widget.conversation.title,
), ),
Padding( Padding(
padding: const EdgeInsets.only(left: 8), padding: const EdgeInsets.only(left: 8),
@ -131,8 +156,11 @@ class ConversationsListRowState extends State<ConversationsListRow> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
widget.name, widget.conversation.title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 17), style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 17,
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -164,17 +192,18 @@ class ConversationsListRowState extends State<ConversationsListRow> {
maxWidth: textWidth, maxWidth: textWidth,
child: _buildLastMessageBody(), child: _buildLastMessageBody(),
), ),
const Spacer(),
Visibility( Visibility(
visible: showBadge, visible: showBadge,
child: const Spacer(),
),
Visibility(
visible: widget.unreadCount > 0,
child: Badge( child: Badge(
badgeContent: Text(badgeText), badgeContent: Text(badgeText),
badgeColor: bubbleColorSent, badgeColor: bubbleColorSent,
), ),
), ),
Visibility(
visible: sentBySelf,
child: _getLastMessageIcon(),
),
], ],
), ),
], ],