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/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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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';
|
||||||
|
@ -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)
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
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 {
|
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)};',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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);',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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>();
|
||||||
|
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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/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(),
|
||||||
|
@ -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}'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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: (_) {
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
@ -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(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
Loading…
Reference in New Issue
Block a user