Compare commits
3 Commits
b0f266bb0a
...
10b86812cd
Author | SHA1 | Date | |
---|---|---|---|
10b86812cd | |||
fe4c794f68 | |||
6115d748e3 |
@ -9,6 +9,7 @@ import 'package:moxplatform/moxplatform.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/service/service.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/blocklist_bloc.dart';
|
||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
||||
@ -99,8 +100,6 @@ void setupBlocs(GlobalKey<NavigatorState> navKey) {
|
||||
// Padding(padding: ..., child: Column(children: [ ... ]))
|
||||
// TODO(Unknown): Theme the switches
|
||||
void main() async {
|
||||
GetIt.I.registerSingleton<Completer<void>>(Completer());
|
||||
|
||||
setupLogging();
|
||||
await setupUIServices();
|
||||
|
||||
@ -190,10 +189,10 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
// Lift the UI block
|
||||
GetIt.I.get<Completer<void>>().complete();
|
||||
|
||||
_setupSharingHandler();
|
||||
|
||||
// Lift the UI block
|
||||
GetIt.I.get<SynchronizedQueue<Map<String, dynamic>?>>().removeQueueLock();
|
||||
}
|
||||
|
||||
Future<void> _handleSharedMedia(SharedMedia media) async {
|
||||
|
@ -5,6 +5,7 @@ import 'package:moxxyv2/service/database/database.dart';
|
||||
import 'package:moxxyv2/service/preferences.dart';
|
||||
import 'package:moxxyv2/shared/cache.dart';
|
||||
import 'package:moxxyv2/shared/models/conversation.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
|
||||
class ConversationService {
|
||||
ConversationService()
|
||||
@ -55,10 +56,8 @@ class ConversationService {
|
||||
|
||||
/// Wrapper around [DatabaseService]'s [updateConversation] that modifies the cache.
|
||||
Future<Conversation> updateConversation(int id, {
|
||||
String? lastMessageBody,
|
||||
int? lastChangeTimestamp,
|
||||
bool? lastMessageRetracted,
|
||||
int? lastMessageId,
|
||||
Message? lastMessage,
|
||||
bool? open,
|
||||
int? unreadCounter,
|
||||
String? avatarUrl,
|
||||
@ -66,21 +65,24 @@ class ConversationService {
|
||||
bool? muted,
|
||||
bool? encrypted,
|
||||
}) async {
|
||||
final conversation = await _getConversationById(id);
|
||||
final newConversation = await GetIt.I.get<DatabaseService>().updateConversation(
|
||||
final conversation = (await _getConversationById(id))!;
|
||||
var newConversation = await GetIt.I.get<DatabaseService>().updateConversation(
|
||||
id,
|
||||
lastMessageBody: lastMessageBody,
|
||||
lastMessageRetracted: lastMessageRetracted,
|
||||
lastMessageId: lastMessageId,
|
||||
lastMessage: lastMessage,
|
||||
lastChangeTimestamp: lastChangeTimestamp,
|
||||
open: open,
|
||||
unreadCounter: unreadCounter,
|
||||
avatarUrl: avatarUrl,
|
||||
chatState: conversation?.chatState ?? ChatState.gone,
|
||||
chatState: conversation.chatState,
|
||||
muted: muted,
|
||||
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);
|
||||
return newConversation;
|
||||
}
|
||||
@ -88,9 +90,7 @@ class ConversationService {
|
||||
/// Wrapper around [DatabaseService]'s [addConversationFromData] that updates the cache.
|
||||
Future<Conversation> addConversationFromData(
|
||||
String title,
|
||||
int lastMessageId,
|
||||
bool lastMessageRetracted,
|
||||
String lastMessageBody,
|
||||
Message? lastMessage,
|
||||
String avatarUrl,
|
||||
String jid,
|
||||
int unreadCounter,
|
||||
@ -101,9 +101,7 @@ class ConversationService {
|
||||
) async {
|
||||
final newConversation = await GetIt.I.get<DatabaseService>().addConversationFromData(
|
||||
title,
|
||||
lastMessageId,
|
||||
lastMessageRetracted,
|
||||
lastMessageBody,
|
||||
lastMessage,
|
||||
avatarUrl,
|
||||
jid,
|
||||
unreadCounter,
|
||||
|
@ -1,5 +1,5 @@
|
||||
const conversationsTable = 'Conversations';
|
||||
const messsagesTable = 'Messages';
|
||||
const messagesTable = 'Messages';
|
||||
const rosterTable = 'RosterItems';
|
||||
const mediaTable = 'SharedMedia';
|
||||
const preferenceTable = 'Preferences';
|
||||
|
@ -19,7 +19,7 @@ Future<void> createDatabase(Database db, int version) async {
|
||||
// Messages
|
||||
await db.execute(
|
||||
'''
|
||||
CREATE TABLE $messsagesTable (
|
||||
CREATE TABLE $messagesTable (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sender TEXT NOT NULL,
|
||||
body TEXT,
|
||||
@ -52,7 +52,7 @@ Future<void> createDatabase(Database db, int version) async {
|
||||
isUploading INTEGER NOT NULL,
|
||||
mediaSize 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,
|
||||
lastChangeTimestamp INTEGER NOT NULL,
|
||||
unreadCounter INTEGER NOT NULL,
|
||||
lastMessageBody TEXT NOT NULL,
|
||||
open INTEGER NOT NULL,
|
||||
muted INTEGER NOT NULL,
|
||||
encrypted 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,
|
||||
message_id INTEGER,
|
||||
FOREIGN KEY (conversation_id) REFERENCES $conversationsTable (id),
|
||||
FOREIGN KEY (message_id) REFERENCES $messsagesTable (id)
|
||||
FOREIGN KEY (message_id) REFERENCES $messagesTable (id)
|
||||
)''',
|
||||
);
|
||||
|
||||
|
@ -8,6 +8,8 @@ import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxyv2/service/database/constants.dart';
|
||||
import 'package:moxxyv2/service/database/creation.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_retraction.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0000_retraction_conversation.dart';
|
||||
@ -58,7 +60,7 @@ class DatabaseService {
|
||||
_db = await openDatabase(
|
||||
dbPath,
|
||||
password: key,
|
||||
version: 6,
|
||||
version: 8,
|
||||
onCreate: createDatabase,
|
||||
onConfigure: configureDatabase,
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
@ -82,6 +84,14 @@ class DatabaseService {
|
||||
_log.finest('Running migration for database version 6');
|
||||
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,17 +117,21 @@ class DatabaseService {
|
||||
final rosterItem = await GetIt.I.get<RosterService>()
|
||||
.getRosterItemByJid(c['jid']! as String);
|
||||
|
||||
Message? lastMessage;
|
||||
if (c['lastMessageId'] != null) {
|
||||
lastMessage = await getMessageById(c['lastMessageId']! as int);
|
||||
}
|
||||
|
||||
tmp.add(
|
||||
Conversation.fromDatabaseJson(
|
||||
c,
|
||||
rosterItem != null,
|
||||
rosterItem?.subscription ?? 'none',
|
||||
sharedMediaRaw,
|
||||
lastMessage,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_log.finest(tmp.toString());
|
||||
|
||||
return tmp;
|
||||
}
|
||||
@ -151,10 +165,8 @@ class DatabaseService {
|
||||
|
||||
/// Updates the conversation with id [id] inside the database.
|
||||
Future<Conversation> updateConversation(int id, {
|
||||
String? lastMessageBody,
|
||||
int? lastChangeTimestamp,
|
||||
bool? lastMessageRetracted,
|
||||
int? lastMessageId,
|
||||
Message? lastMessage,
|
||||
bool? open,
|
||||
int? unreadCounter,
|
||||
String? avatarUrl,
|
||||
@ -176,15 +188,8 @@ class DatabaseService {
|
||||
orderBy: 'timestamp DESC',
|
||||
)).map(SharedMedium.fromDatabaseJson);
|
||||
|
||||
//await c.sharedMedia.load();
|
||||
if (lastMessageBody != null) {
|
||||
c['lastMessageBody'] = lastMessageBody;
|
||||
}
|
||||
if (lastMessageRetracted != null) {
|
||||
c['lastMessageRetracted'] = boolToInt(lastMessageRetracted);
|
||||
}
|
||||
if (lastMessageId != null) {
|
||||
c['lastMessageId'] = lastMessageId;
|
||||
if (lastMessage != null) {
|
||||
c['lastMessageId'] = lastMessage.id;
|
||||
}
|
||||
if (lastChangeTimestamp != null) {
|
||||
c['lastChangeTimestamp'] = lastChangeTimestamp;
|
||||
@ -218,6 +223,7 @@ class DatabaseService {
|
||||
rosterItem != null,
|
||||
rosterItem?.subscription ?? 'none',
|
||||
sharedMedia.map((m) => m.toJson()).toList(),
|
||||
lastMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@ -225,9 +231,7 @@ class DatabaseService {
|
||||
/// [Conversation] object can carry its database id.
|
||||
Future<Conversation> addConversationFromData(
|
||||
String title,
|
||||
int lastMessageId,
|
||||
bool lastMessageRetracted,
|
||||
String lastMessageBody,
|
||||
Message? lastMessage,
|
||||
String avatarUrl,
|
||||
String jid,
|
||||
int unreadCounter,
|
||||
@ -239,9 +243,7 @@ class DatabaseService {
|
||||
final rosterItem = await GetIt.I.get<RosterService>().getRosterItemByJid(jid);
|
||||
final conversation = Conversation(
|
||||
title,
|
||||
lastMessageId,
|
||||
lastMessageRetracted,
|
||||
lastMessageBody,
|
||||
lastMessage,
|
||||
avatarUrl,
|
||||
jid,
|
||||
unreadCounter,
|
||||
|
13
lib/service/database/migrations/0000_conversations.dart
Normal file
13
lib/service/database/migrations/0000_conversations.dart
Normal 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 '';"
|
||||
);
|
||||
|
||||
}
|
18
lib/service/database/migrations/0000_conversations2.dart
Normal file
18
lib/service/database/migrations/0000_conversations2.dart
Normal 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;"
|
||||
);
|
||||
}
|
@ -6,6 +6,6 @@ import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||
Future<void> upgradeFromV3ToV4(Database db) async {
|
||||
// Mark all messages as not retracted
|
||||
await db.execute(
|
||||
'ALTER TABLE $messsagesTable ADD COLUMN isRetracted INTEGER DEFAULT ${boolToInt(false)};',
|
||||
'ALTER TABLE $messagesTable ADD COLUMN isRetracted INTEGER DEFAULT ${boolToInt(false)};',
|
||||
);
|
||||
}
|
||||
|
@ -5,6 +5,6 @@ import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||
Future<void> upgradeFromV5ToV6(Database db) async {
|
||||
// Allow shared media to reference a message
|
||||
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);',
|
||||
);
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import 'package:moxxyv2/shared/eventhandler.dart';
|
||||
import 'package:moxxyv2/shared/events.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/preferences.dart';
|
||||
import 'package:moxxyv2/shared/synchronized_queue.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
void setupBackgroundEventHandler() {
|
||||
@ -68,6 +69,9 @@ void setupBackgroundEventHandler() {
|
||||
]);
|
||||
|
||||
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 {
|
||||
@ -141,13 +145,6 @@ Future<PreStartDoneEvent> _buildPreStartDoneEvent(PreferencesState preferences)
|
||||
|
||||
Future<void> performPreStart(PerformPreStartCommand command, { dynamic extra }) async {
|
||||
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();
|
||||
|
||||
// Set the locale very early
|
||||
@ -209,9 +206,7 @@ Future<void> performAddConversation(AddConversationCommand command, { dynamic ex
|
||||
} else {
|
||||
final conversation = await cs.addConversationFromData(
|
||||
command.title,
|
||||
-1,
|
||||
false,
|
||||
command.lastMessageBody,
|
||||
null,
|
||||
command.avatarUrl,
|
||||
command.jid,
|
||||
0,
|
||||
@ -335,9 +330,7 @@ Future<void> performAddContact(AddContactCommand command, { dynamic extra }) asy
|
||||
} else {
|
||||
final c = await cs.addConversationFromData(
|
||||
jid.split('@')[0],
|
||||
-1,
|
||||
false,
|
||||
'',
|
||||
null,
|
||||
'',
|
||||
jid,
|
||||
0,
|
||||
@ -621,14 +614,10 @@ Future<void> performMarkConversationAsRead(MarkConversationAsReadCommand command
|
||||
|
||||
sendEvent(ConversationUpdatedEvent(conversation: conversation));
|
||||
|
||||
final msg = await GetIt.I.get<MessageService>().getMessageById(
|
||||
conversation.jid,
|
||||
conversation.lastMessageId,
|
||||
);
|
||||
if (msg != null) {
|
||||
if (conversation.lastMessage != null) {
|
||||
await GetIt.I.get<XmppService>().sendReadMarker(
|
||||
conversation.jid,
|
||||
msg.sid,
|
||||
conversation.lastMessage!.sid,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -239,17 +239,16 @@ class MessageService {
|
||||
mediaSize: null,
|
||||
isRetracted: true,
|
||||
thumbnailData: null,
|
||||
body: '',
|
||||
);
|
||||
sendEvent(MessageUpdatedEvent(message: retractedMessage));
|
||||
|
||||
final cs = GetIt.I.get<ConversationService>();
|
||||
final conversation = await cs.getConversationByJid(conversationJid);
|
||||
if (conversation != null) {
|
||||
if (conversation.lastMessageId == msg.id) {
|
||||
var newConversation = await cs.updateConversation(
|
||||
conversation.id,
|
||||
lastMessageBody: '',
|
||||
lastMessageRetracted: true,
|
||||
if (conversation.lastMessage?.id == msg.id) {
|
||||
var newConversation = conversation.copyWith(
|
||||
lastMessage: retractedMessage,
|
||||
);
|
||||
|
||||
if (isMedia) {
|
||||
@ -260,7 +259,6 @@ class MessageService {
|
||||
return medium.messageId != msg.id;
|
||||
}).toList(),
|
||||
);
|
||||
GetIt.I.get<ConversationService>().setConversation(newConversation);
|
||||
|
||||
// Delete the file if we downloaded it
|
||||
if (mediaUrl != null) {
|
||||
@ -271,6 +269,7 @@ class MessageService {
|
||||
}
|
||||
}
|
||||
|
||||
cs.setConversation(newConversation);
|
||||
sendEvent(
|
||||
ConversationUpdatedEvent(
|
||||
conversation: newConversation,
|
||||
|
@ -35,6 +35,7 @@ import 'package:moxxyv2/shared/commands.dart';
|
||||
import 'package:moxxyv2/shared/eventhandler.dart';
|
||||
import 'package:moxxyv2/shared/events.dart';
|
||||
import 'package:moxxyv2/shared/logging.dart';
|
||||
import 'package:moxxyv2/shared/synchronized_queue.dart';
|
||||
import 'package:moxxyv2/ui/events.dart' as ui_events;
|
||||
|
||||
Future<void> initializeServiceIfNeeded() async {
|
||||
@ -47,7 +48,7 @@ Future<void> initializeServiceIfNeeded() async {
|
||||
}
|
||||
|
||||
logger.info('Attaching to service...');
|
||||
await handler.attach(ui_events.handleIsolateEvent);
|
||||
await handler.attach(ui_events.receiveIsolateEvent);
|
||||
logger.info('Done');
|
||||
|
||||
// ignore: cascade_invocations
|
||||
@ -62,7 +63,7 @@ Future<void> initializeServiceIfNeeded() async {
|
||||
logger.info('Service is not running. Initializing service... ');
|
||||
await handler.start(
|
||||
entrypoint,
|
||||
handleUiEvent,
|
||||
receiveUIEvent,
|
||||
ui_events.handleIsolateEvent,
|
||||
);
|
||||
}
|
||||
@ -72,7 +73,7 @@ Future<void> initializeServiceIfNeeded() async {
|
||||
/// logging what we send.
|
||||
void sendEvent(BackgroundEvent event, { String? id }) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@ -130,16 +131,13 @@ Future<void> initUDPLogger() async {
|
||||
/// The entrypoint for all platforms after the platform specific initilization is done.
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> entrypoint() async {
|
||||
// Register the lock
|
||||
GetIt.I.registerSingleton<Completer<void>>(Completer());
|
||||
setupLogging();
|
||||
setupBackgroundEventHandler();
|
||||
|
||||
// Register singletons
|
||||
GetIt.I.registerSingleton<Logger>(Logger('MoxxyService'));
|
||||
GetIt.I.registerSingleton<UDPLogger>(UDPLogger());
|
||||
GetIt.I.registerSingleton<LanguageService>(LanguageService());
|
||||
|
||||
setupLogging();
|
||||
setupBackgroundEventHandler();
|
||||
|
||||
// Initialize the database
|
||||
GetIt.I.registerSingleton<DatabaseService>(DatabaseService());
|
||||
@ -242,13 +240,15 @@ Future<void> entrypoint() async {
|
||||
);
|
||||
}
|
||||
|
||||
GetIt.I.get<Logger>().finest('Resolving startup future');
|
||||
GetIt.I.get<Completer<void>>().complete();
|
||||
|
||||
unawaited(GetIt.I.get<SynchronizedQueue<Map<String, dynamic>?>>().removeQueueLock());
|
||||
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
|
||||
final log = GetIt.I.get<Logger>();
|
||||
|
||||
|
@ -139,7 +139,7 @@ class XmppService {
|
||||
final cs = GetIt.I.get<ConversationService>();
|
||||
final conn = GetIt.I.get<XmppConnection>();
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
|
||||
for (final recipient in recipients) {
|
||||
final sid = conn.generateId();
|
||||
final originId = conn.generateId();
|
||||
@ -158,9 +158,7 @@ class XmppService {
|
||||
);
|
||||
final newConversation = await cs.updateConversation(
|
||||
conversation.id,
|
||||
lastMessageBody: body,
|
||||
lastMessageId: message.id,
|
||||
lastMessageRetracted: false,
|
||||
lastMessage: message,
|
||||
lastChangeTimestamp: timestamp,
|
||||
);
|
||||
|
||||
@ -364,7 +362,7 @@ class XmppService {
|
||||
// Recipient -> Should encrypt
|
||||
final encrypt = <String, bool>{};
|
||||
// Recipient -> Last message Id
|
||||
final lastMessageIds = <String, int>{};
|
||||
final lastMessages = <String, Message>{};
|
||||
|
||||
// Create the messages and shared media entries
|
||||
final conn = GetIt.I.get<XmppConnection>();
|
||||
@ -412,7 +410,7 @@ class XmppService {
|
||||
}
|
||||
|
||||
if (path == paths.last) {
|
||||
lastMessageIds[recipient] = msg.id;
|
||||
lastMessages[recipient] = msg;
|
||||
}
|
||||
|
||||
sendEvent(MessageAddedEvent(message: msg));
|
||||
@ -424,14 +422,12 @@ class XmppService {
|
||||
final sharedMediaMap = <String, List<SharedMedium>>{};
|
||||
final rs = GetIt.I.get<RosterService>();
|
||||
for (final recipient in recipients) {
|
||||
final lastFileMime = lookupMimeType(paths.last);
|
||||
final conversation = await cs.getConversationByJid(recipient);
|
||||
if (conversation != null) {
|
||||
// Update conversation
|
||||
var updatedConversation = await cs.updateConversation(
|
||||
conversation.id,
|
||||
lastMessageBody: mimeTypeToEmoji(lastFileMime),
|
||||
lastMessageId: lastMessageIds[recipient],
|
||||
lastMessage: lastMessages[recipient],
|
||||
lastChangeTimestamp: DateTime.now().millisecondsSinceEpoch,
|
||||
open: true,
|
||||
);
|
||||
@ -452,9 +448,7 @@ class XmppService {
|
||||
var newConversation = await cs.addConversationFromData(
|
||||
// TODO(Unknown): Should we use the JID parser?
|
||||
rosterItem?.title ?? recipient.split('@').first,
|
||||
lastMessageIds[recipient]!,
|
||||
false,
|
||||
mimeTypeToEmoji(lastFileMime),
|
||||
lastMessages[recipient],
|
||||
rosterItem?.avatarUrl ?? '',
|
||||
recipient,
|
||||
0,
|
||||
@ -653,9 +647,7 @@ class XmppService {
|
||||
final bare = event.from.toBare();
|
||||
final conv = await cs.addConversationFromData(
|
||||
bare.toString().split('@')[0],
|
||||
-1,
|
||||
false,
|
||||
'',
|
||||
null,
|
||||
'', // TODO(Unknown): avatarUrl
|
||||
bare.toString(),
|
||||
0,
|
||||
@ -693,7 +685,9 @@ class XmppService {
|
||||
|
||||
final db = GetIt.I.get<DatabaseService>();
|
||||
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) {
|
||||
_log.warning('Did not find the message in the database!');
|
||||
return;
|
||||
@ -704,8 +698,15 @@ class XmppService {
|
||||
received: dbMsg.received || event.type == 'received' || event.type == 'displayed',
|
||||
displayed: dbMsg.displayed || event.type == 'displayed',
|
||||
);
|
||||
|
||||
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 {
|
||||
@ -989,10 +990,8 @@ class XmppService {
|
||||
// The conversation exists, so we can just update it
|
||||
final newConversation = await cs.updateConversation(
|
||||
conversation.id,
|
||||
lastMessageBody: conversationBody,
|
||||
lastMessage: message,
|
||||
lastChangeTimestamp: messageTimestamp,
|
||||
lastMessageId: message.id,
|
||||
lastMessageRetracted: false,
|
||||
// Do not increment the counter for messages we sent ourselves (via Carbons)
|
||||
// or if we have the chat currently opened
|
||||
unreadCounter: isConversationOpened || sent
|
||||
@ -1017,9 +1016,7 @@ class XmppService {
|
||||
// The conversation does not exist, so we must create it
|
||||
final newConversation = await cs.addConversationFromData(
|
||||
rosterItem?.title ?? conversationJid.split('@')[0],
|
||||
message.id,
|
||||
false,
|
||||
conversationBody,
|
||||
message,
|
||||
rosterItem?.avatarUrl ?? '',
|
||||
conversationJid,
|
||||
sent ? 0 : 1,
|
||||
@ -1128,11 +1125,20 @@ class XmppService {
|
||||
Future<void> _onStanzaAcked(StanzaAckedEvent event, { dynamic extra }) async {
|
||||
final jid = JID.fromString(event.stanza.to!).toBare().toString();
|
||||
final ms = GetIt.I.get<MessageService>();
|
||||
final cs = GetIt.I.get<ConversationService>();
|
||||
final msg = await ms.getMessageByStanzaId(jid, event.stanza.id!);
|
||||
if (msg != null) {
|
||||
// Ack the message
|
||||
final newMsg = await ms.updateMessage(msg.id, acked: true);
|
||||
|
||||
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 {
|
||||
_log.finest('Wanted to mark message as acked but did not find the message to ack');
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxyv2/service/database/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/media.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
|
||||
part 'conversation.freezed.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
|
||||
class Conversation with _$Conversation {
|
||||
factory Conversation(
|
||||
String title,
|
||||
// NOTE: The internal database Id of the message
|
||||
int lastMessageId,
|
||||
bool lastMessageRetracted,
|
||||
String lastMessageBody,
|
||||
@ConversationMessageConverter() Message? lastMessage,
|
||||
String avatarUrl,
|
||||
String jid,
|
||||
int unreadCounter,
|
||||
@ -52,7 +66,7 @@ class Conversation with _$Conversation {
|
||||
/// 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({
|
||||
...json,
|
||||
'muted': intToBool(json['muted']! as int),
|
||||
@ -62,7 +76,9 @@ class Conversation with _$Conversation {
|
||||
'subscription': subscription,
|
||||
'encrypted': intToBool(json['encrypted']! as int),
|
||||
'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('sharedMedia')
|
||||
..remove('inRoster')
|
||||
..remove('subscription');
|
||||
..remove('subscription')
|
||||
..remove('lastMessage');
|
||||
|
||||
return {
|
||||
...map,
|
||||
'open': boolToInt(open),
|
||||
'muted': boolToInt(muted),
|
||||
'encrypted': boolToInt(encrypted),
|
||||
'lastMessageRetracted': boolToInt(lastMessageRetracted),
|
||||
'lastMessage': lastMessage?.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
43
lib/shared/synchronized_queue.dart
Normal file
43
lib/shared/synchronized_queue.dart
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import 'package:moxplatform/moxplatform.dart';
|
||||
import 'package:moxxyv2/shared/commands.dart';
|
||||
import 'package:moxxyv2/shared/eventhandler.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/conversation_bloc.dart' as conversation;
|
||||
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart' as conversations;
|
||||
@ -34,6 +35,11 @@ void setupEventHandler() {
|
||||
]);
|
||||
|
||||
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 {
|
||||
@ -50,7 +56,7 @@ Future<void> handleIsolateEvent(Map<String, dynamic>? json) async {
|
||||
event,
|
||||
);
|
||||
|
||||
log.finest('S2F: $event');
|
||||
log.finest('<-- ${(event).toString()}');
|
||||
|
||||
// First attempt to deal with awaitables
|
||||
var found = false;
|
||||
@ -136,9 +142,6 @@ Future<void> onSelfAvatarChanged(SelfAvatarChangedEvent event, { dynamic extra }
|
||||
}
|
||||
|
||||
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(
|
||||
PerformPreStartCommand(
|
||||
systemLocaleCode: WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag(),
|
||||
|
@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
||||
import 'package:flutter_vibrate/flutter_vibrate.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
||||
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
||||
@ -55,15 +54,9 @@ class ConversationsPageState extends State<ConversationsPage> with TickerProvide
|
||||
itemBuilder: (_context, index) {
|
||||
final item = state.conversations[index];
|
||||
final row = ConversationsListRow(
|
||||
item.avatarUrl,
|
||||
item.title,
|
||||
item.lastMessageBody,
|
||||
item.unreadCounter,
|
||||
maxTextWidth,
|
||||
item.lastChangeTimestamp,
|
||||
item,
|
||||
true,
|
||||
typingIndicator: item.chatState == ChatState.composing,
|
||||
lastMessageRetracted: item.lastMessageRetracted,
|
||||
key: ValueKey('conversationRow;${item.jid}'),
|
||||
);
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:moxxmpp/moxxmpp.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/constants.dart';
|
||||
import 'package:moxxyv2/ui/helpers.dart';
|
||||
@ -94,7 +95,27 @@ class NewConversationPage extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -2,8 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.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/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/share_selection_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
@ -68,14 +69,26 @@ class ShareSelectionPage extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
child: ConversationsListRow(
|
||||
item.avatarPath,
|
||||
item.title,
|
||||
item.jid,
|
||||
0,
|
||||
maxTextWidth,
|
||||
timestampNever,
|
||||
Conversation(
|
||||
item.title,
|
||||
null,
|
||||
item.avatarPath,
|
||||
item.jid,
|
||||
0,
|
||||
0,
|
||||
[],
|
||||
0,
|
||||
true,
|
||||
true,
|
||||
'',
|
||||
false,
|
||||
false,
|
||||
ChatState.gone,
|
||||
),
|
||||
false,
|
||||
showLock: item.isEncrypted,
|
||||
showTimestamp: false,
|
||||
extra: Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (_) {
|
||||
|
@ -15,10 +15,6 @@ import 'package:share_handler/share_handler.dart';
|
||||
|
||||
/// Handler for when we received a [PreStartDoneEvent].
|
||||
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(
|
||||
PreferencesChangedEvent(result.preferences),
|
||||
);
|
||||
|
@ -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
|
||||
// the event receiver
|
||||
class UIDataService {
|
||||
|
||||
UIDataService() : isLoggedIn = false;
|
||||
|
||||
bool isLoggedIn;
|
||||
|
@ -1,39 +1,33 @@
|
||||
import 'dart:async';
|
||||
import 'package:badges/badges.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/shared/constants.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/conversation.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/chat/typing.dart';
|
||||
|
||||
class ConversationsListRow extends StatefulWidget {
|
||||
const ConversationsListRow(
|
||||
this.avatarUrl,
|
||||
this.name,
|
||||
this.lastMessageBody,
|
||||
this.unreadCount,
|
||||
this.maxTextWidth,
|
||||
this.lastChangeTimestamp,
|
||||
this.conversation,
|
||||
this.update, {
|
||||
this.showTimestamp = true,
|
||||
this.showLock = false,
|
||||
this.typingIndicator = false,
|
||||
this.extra,
|
||||
this.lastMessageRetracted = false,
|
||||
super.key,
|
||||
}
|
||||
);
|
||||
final String avatarUrl;
|
||||
final String name;
|
||||
final bool lastMessageRetracted;
|
||||
final String lastMessageBody;
|
||||
final int unreadCount;
|
||||
final Conversation conversation;
|
||||
final double maxTextWidth;
|
||||
final int lastChangeTimestamp;
|
||||
final bool update; // Should a timer run to update the timestamp
|
||||
final bool typingIndicator;
|
||||
final bool showLock;
|
||||
final bool showTimestamp;
|
||||
final Widget? extra;
|
||||
|
||||
@override
|
||||
@ -51,23 +45,23 @@ class ConversationsListRowState extends State<ConversationsListRow> {
|
||||
final _now = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
_timestampString = formatConversationTimestamp(
|
||||
widget.lastChangeTimestamp,
|
||||
widget.conversation.lastChangeTimestamp,
|
||||
_now,
|
||||
);
|
||||
|
||||
// NOTE: We could also check and run the timer hourly, but who has a messenger on the
|
||||
// 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) {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
setState(() {
|
||||
_timestampString = formatConversationTimestamp(
|
||||
widget.lastChangeTimestamp,
|
||||
widget.conversation.lastChangeTimestamp,
|
||||
now,
|
||||
);
|
||||
});
|
||||
|
||||
if (now - widget.lastChangeTimestamp >= 60 * Duration.millisecondsPerMinute) {
|
||||
if (now - widget.conversation.lastChangeTimestamp >= 60 * Duration.millisecondsPerMinute) {
|
||||
_updateTimer!.cancel();
|
||||
_updateTimer = null;
|
||||
}
|
||||
@ -87,38 +81,69 @@ class ConversationsListRowState extends State<ConversationsListRow> {
|
||||
}
|
||||
|
||||
Widget _buildLastMessageBody() {
|
||||
if (widget.typingIndicator) {
|
||||
if (widget.conversation.chatState == ChatState.composing) {
|
||||
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(
|
||||
widget.lastMessageRetracted ?
|
||||
t.messages.retracted :
|
||||
widget.lastMessageBody,
|
||||
body,
|
||||
maxLines: 1,
|
||||
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
|
||||
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
|
||||
// for every conversation
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final width = screenWidth - 24 - 70;
|
||||
final textWidth = screenWidth * 0.6;
|
||||
|
||||
final showTimestamp = widget.lastChangeTimestamp != timestampNever;
|
||||
final showBadge = widget.unreadCount > 0;
|
||||
final showTimestamp = widget.conversation.lastChangeTimestamp != timestampNever && widget.showTimestamp;
|
||||
final sentBySelf = widget.conversation.lastMessage?.sender == GetIt.I.get<UIDataService>().ownJid!;
|
||||
|
||||
final showBadge = widget.conversation.unreadCounter > 0 && !sentBySelf;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
AvatarWrapper(
|
||||
radius: 35,
|
||||
avatarUrl: widget.avatarUrl,
|
||||
altText: widget.name,
|
||||
avatarUrl: widget.conversation.avatarUrl,
|
||||
altText: widget.conversation.title,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
@ -131,8 +156,11 @@ class ConversationsListRowState extends State<ConversationsListRow> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 17),
|
||||
widget.conversation.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 17,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@ -164,17 +192,18 @@ class ConversationsListRowState extends State<ConversationsListRow> {
|
||||
maxWidth: textWidth,
|
||||
child: _buildLastMessageBody(),
|
||||
),
|
||||
const Spacer(),
|
||||
Visibility(
|
||||
visible: showBadge,
|
||||
child: const Spacer(),
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.unreadCount > 0,
|
||||
child: Badge(
|
||||
badgeContent: Text(badgeText),
|
||||
badgeColor: bubbleColorSent,
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: sentBySelf,
|
||||
child: _getLastMessageIcon(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
Loading…
Reference in New Issue
Block a user