xmpp: RECEIVE MESSAGES!
This commit is contained in:
parent
f22d042255
commit
1acc2630b4
@ -3,7 +3,7 @@ import "package:moxxyv2/isar.g.dart";
|
||||
|
||||
@Collection()
|
||||
@Name("Conversation")
|
||||
class Conversation {
|
||||
class DBConversation {
|
||||
int? id;
|
||||
|
||||
@Index(caseSensitive: false)
|
||||
|
17
lib/db/message.dart
Normal file
17
lib/db/message.dart
Normal file
@ -0,0 +1,17 @@
|
||||
import "package:isar/isar.dart";
|
||||
import "package:moxxyv2/isar.g.dart";
|
||||
|
||||
@Collection()
|
||||
@Name("Message")
|
||||
class DBMessage {
|
||||
int? id;
|
||||
|
||||
@Index(caseSensitive: false)
|
||||
late String from;
|
||||
|
||||
late int timestamp;
|
||||
|
||||
late String body;
|
||||
|
||||
late bool sent;
|
||||
}
|
@ -21,10 +21,18 @@ String padInt(int i) {
|
||||
* returned true.
|
||||
*/
|
||||
bool listContains<T>(List<T> list, bool Function(T element) test) {
|
||||
return firstWhereOrNull<T>(list, test) != null;
|
||||
}
|
||||
|
||||
/*
|
||||
* A wrapper around List<T>.firstWhere that does not throw but instead just
|
||||
* return null if test never returned true
|
||||
*/
|
||||
T? firstWhereOrNull<T>(List<T> list, bool Function(T element) test) {
|
||||
try {
|
||||
return list.firstWhere(test) != null;
|
||||
return list.firstWhere(test);
|
||||
} catch(e) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,15 +3,17 @@ class Message {
|
||||
final int timestamp; // NOTE: Milliseconds since Epoch
|
||||
final String from;
|
||||
final bool sent;
|
||||
final int id; // Database ID
|
||||
|
||||
const Message({ required this.from, required this.body, required this.timestamp, required this.sent });
|
||||
const Message({ required this.from, required this.body, required this.timestamp, required this.sent, required this.id });
|
||||
|
||||
Message copyWith({ String? from, String? body, int? timestamp }) {
|
||||
return Message(
|
||||
from: from ?? this.from,
|
||||
body: body ?? this.body,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
sent: this.sent
|
||||
sent: this.sent,
|
||||
id: this.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import "package:moxxyv2/models/message.dart";
|
||||
import "package:moxxyv2/xmpp/jid.dart";
|
||||
|
||||
class SetShowSendButtonAction {
|
||||
final bool show;
|
||||
|
||||
@ -20,6 +23,21 @@ class SendMessageAction {
|
||||
SendMessageAction({ required this.from, required this.body, required this.timestamp, required this.jid, required this.cid });
|
||||
}
|
||||
|
||||
class ReceiveMessageAction {
|
||||
final String body;
|
||||
final int timestamp;
|
||||
final FullJID from;
|
||||
final String jid;
|
||||
|
||||
ReceiveMessageAction({ required this.from, required this.body, required this.timestamp, required this.jid });
|
||||
}
|
||||
|
||||
class AddMessageAction {
|
||||
final Message message;
|
||||
|
||||
AddMessageAction({ required this.message });
|
||||
}
|
||||
|
||||
class CloseConversationAction {
|
||||
final String jid;
|
||||
final int id;
|
||||
|
@ -5,24 +5,14 @@ import "package:moxxyv2/redux/conversation/state.dart";
|
||||
import "package:moxxyv2/redux/conversation/actions.dart";
|
||||
|
||||
HashMap<String, List<Message>> messageReducer(HashMap<String, List<Message>> state, dynamic action) {
|
||||
if (action is SendMessageAction) {
|
||||
HashMap<String, List<Message>> map = HashMap<String, List<Message>>()..addAll(state);
|
||||
|
||||
Message msg = Message(
|
||||
from: action.from,
|
||||
body: action.body,
|
||||
timestamp: action.timestamp,
|
||||
sent: true
|
||||
);
|
||||
|
||||
String jid = action.jid;
|
||||
if (!map.containsKey(jid)) {
|
||||
map[jid] = [ msg ];
|
||||
return map;
|
||||
if (action is AddMessageAction) {
|
||||
if (!state.containsKey(action.message.from)) {
|
||||
state[action.message.from] = List.from([ action.message ]);
|
||||
} else {
|
||||
state[action.message.from] = state[action.message.from]!..add(action.message);
|
||||
}
|
||||
|
||||
map[jid]!.add(msg);
|
||||
return map;
|
||||
return state;
|
||||
}
|
||||
|
||||
return state;
|
||||
|
@ -1,16 +1,13 @@
|
||||
import "dart:collection";
|
||||
import "package:moxxyv2/models/conversation.dart";
|
||||
|
||||
class AddConversationAction {
|
||||
final String title;
|
||||
final String lastMessageBody;
|
||||
final String avatarUrl;
|
||||
final String jid;
|
||||
final int id;
|
||||
final int unreadCounter;
|
||||
final List<String> sharedMediaPaths;
|
||||
final int lastChangeTimestamp;
|
||||
final bool triggeredByDatabase;
|
||||
final bool open;
|
||||
Conversation conversation;
|
||||
|
||||
AddConversationAction({ required this.title, required this.lastMessageBody, required this.avatarUrl, required this.jid, required this.sharedMediaPaths, required this.lastChangeTimestamp, required this.id, this.unreadCounter = 0, this.triggeredByDatabase = false, required this.open });
|
||||
AddConversationAction({ required this.conversation });
|
||||
}
|
||||
|
||||
class UpdateConversationAction {
|
||||
Conversation conversation;
|
||||
|
||||
UpdateConversationAction({ required this.conversation });
|
||||
}
|
||||
|
@ -2,29 +2,13 @@ import "package:moxxyv2/redux/state.dart";
|
||||
import "package:moxxyv2/redux/conversations/actions.dart";
|
||||
import "package:moxxyv2/redux/conversation/actions.dart";
|
||||
import "package:moxxyv2/repositories/conversation.dart";
|
||||
import "package:moxxyv2/models/conversation.dart";
|
||||
|
||||
import "package:redux/redux.dart";
|
||||
import "package:flutter_redux_navigation/flutter_redux_navigation.dart";
|
||||
import "package:get_it/get_it.dart";
|
||||
|
||||
void conversationsMiddleware(Store<MoxxyState> store, action, NextDispatcher next) {
|
||||
var repo = GetIt.I.get<DatabaseRepository>();
|
||||
|
||||
if (action is AddConversationAction && !action.triggeredByDatabase) {
|
||||
if (repo.hasConversation(action.id)) {
|
||||
// TODO
|
||||
} else {
|
||||
repo.addConversationFromAction(action);
|
||||
}
|
||||
} else if (action is SendMessageAction) {
|
||||
if (repo.hasConversation(action.cid)) {
|
||||
repo.updateConversation(id: action.cid, lastMessageBody: action.body, lastChangeTimestamp: action.timestamp);
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
} else if (action is CloseConversationAction) {
|
||||
store.dispatch(NavigateToAction.replace("/conversations"));
|
||||
}
|
||||
|
||||
void conversationsMiddleware(Store<MoxxyState> store, action, NextDispatcher next) async {
|
||||
|
||||
next(action);
|
||||
}
|
||||
|
@ -5,30 +5,16 @@ import "package:moxxyv2/redux/conversation/actions.dart";
|
||||
|
||||
List<Conversation> conversationReducer(List<Conversation> state, dynamic action) {
|
||||
if (action is AddConversationAction) {
|
||||
state.add(Conversation(
|
||||
title: action.title,
|
||||
lastMessageBody: action.lastMessageBody,
|
||||
avatarUrl: action.avatarUrl,
|
||||
jid: action.jid,
|
||||
// TODO: Correct?
|
||||
unreadCounter: 0,
|
||||
sharedMediaPaths: action.sharedMediaPaths,
|
||||
lastChangeTimestamp: action.lastChangeTimestamp,
|
||||
open: action.open,
|
||||
id: action.id
|
||||
));
|
||||
} else if (action is SendMessageAction) {
|
||||
return state.map((element) {
|
||||
if (element.jid == action.jid) {
|
||||
return element.copyWith(lastMessageBody: action.body, lastChangeTimestamp: action.timestamp);
|
||||
return state..add(action.conversation);
|
||||
} else if (action is UpdateConversationAction) {
|
||||
return state.map((c) {
|
||||
if (c.id == action.conversation.id) {
|
||||
return action.conversation;
|
||||
}
|
||||
|
||||
return element;
|
||||
return c;
|
||||
}).toList();
|
||||
} else if (action is CloseConversationAction) {
|
||||
// TODO: Yikes.
|
||||
return state.map((element) => element.jid == action.jid ? element.copyWith(open: false) : element).toList().where((element) => element.open).toList();
|
||||
}
|
||||
|
||||
|
||||
return state;
|
||||
}
|
||||
|
5
lib/redux/messages/actions.dart
Normal file
5
lib/redux/messages/actions.dart
Normal file
@ -0,0 +1,5 @@
|
||||
class LoadMessagesAction {
|
||||
final String jid;
|
||||
|
||||
LoadMessagesAction({ required this.jid });
|
||||
}
|
65
lib/redux/messages/middleware.dart
Normal file
65
lib/redux/messages/middleware.dart
Normal file
@ -0,0 +1,65 @@
|
||||
import "package:moxxyv2/redux/state.dart";
|
||||
import "package:moxxyv2/redux/conversations/actions.dart";
|
||||
import "package:moxxyv2/redux/conversation/actions.dart";
|
||||
import "package:moxxyv2/repositories/conversation.dart";
|
||||
import "package:moxxyv2/models/conversation.dart";
|
||||
import "package:moxxyv2/models/message.dart";
|
||||
import "package:moxxyv2/redux/messages/actions.dart";
|
||||
import "package:moxxyv2/helpers.dart";
|
||||
|
||||
import "package:redux/redux.dart";
|
||||
import "package:flutter_redux_navigation/flutter_redux_navigation.dart";
|
||||
import "package:get_it/get_it.dart";
|
||||
|
||||
void messageMiddleware(Store<MoxxyState> store, action, NextDispatcher next) async {
|
||||
if (action is ReceiveMessageAction) {
|
||||
// TODO: Check if the conversation already exists
|
||||
final repo = GetIt.I.get<DatabaseRepository>();
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final bareJidString = action.from.toBare().toString();
|
||||
|
||||
final message = await repo.addMessageFromData(
|
||||
action.body,
|
||||
now,
|
||||
bareJidString,
|
||||
false
|
||||
);
|
||||
|
||||
final existantConversation = firstWhereOrNull(store.state.conversations, (Conversation c) => c.jid == bareJidString);
|
||||
if (existantConversation == null) {
|
||||
final conversation = await repo.addConversationFromData(
|
||||
action.from.local,
|
||||
action.body,
|
||||
"",
|
||||
bareJidString,
|
||||
1,
|
||||
now,
|
||||
[],
|
||||
true
|
||||
);
|
||||
|
||||
repo.loadedConversations.add(bareJidString);
|
||||
store.dispatch(AddConversationAction(conversation: conversation));
|
||||
} else {
|
||||
await repo.updateConversation(
|
||||
id: existantConversation.id,
|
||||
lastMessageBody: action.body,
|
||||
lastChangeTimestamp: now,
|
||||
unreadCounter: existantConversation.unreadCounter + 1
|
||||
);
|
||||
store.dispatch(UpdateConversationAction(
|
||||
conversation: existantConversation.copyWith(
|
||||
lastMessageBody: action.body,
|
||||
lastChangeTimestamp: now,
|
||||
unreadCounter: existantConversation.unreadCounter + 1
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
store.dispatch(AddMessageAction(message: message));
|
||||
} else if (action is LoadMessagesAction) {
|
||||
GetIt.I.get<DatabaseRepository>().loadMessagesForJid(action.jid);
|
||||
}
|
||||
|
||||
next(action);
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
import "dart:collection";
|
||||
|
||||
import "package:moxxyv2/db/conversation.dart" as db;
|
||||
import "package:moxxyv2/models/conversation.dart" as model;
|
||||
import "package:moxxyv2/db/conversation.dart";
|
||||
import "package:moxxyv2/db/message.dart";
|
||||
import "package:moxxyv2/models/conversation.dart";
|
||||
import "package:moxxyv2/models/message.dart";
|
||||
import "package:moxxyv2/redux/state.dart";
|
||||
import "package:moxxyv2/redux/conversations/actions.dart";
|
||||
import "package:moxxyv2/redux/conversation/actions.dart";
|
||||
|
||||
import "package:isar/isar.dart";
|
||||
import "package:redux/redux.dart";
|
||||
@ -16,16 +19,18 @@ class DatabaseRepository {
|
||||
final Isar isar;
|
||||
final Store<MoxxyState> store;
|
||||
|
||||
final HashMap<int, db.Conversation> _cache = HashMap();
|
||||
final HashMap<int, DBConversation> _cache = HashMap();
|
||||
final List<String> loadedConversations = List.empty(growable: true);
|
||||
|
||||
DatabaseRepository({ required this.isar, required this.store });
|
||||
|
||||
|
||||
Future<void> loadConversations() async {
|
||||
var conversations = await this.isar.conversations.where().findAll();
|
||||
|
||||
var conversations = await this.isar.dBConversations.where().findAll();
|
||||
|
||||
conversations.forEach((c) {
|
||||
this._cache[c.id!] = c;
|
||||
this.store.dispatch(AddConversationAction(
|
||||
conversation: Conversation(
|
||||
id: c.id!,
|
||||
title: c.title,
|
||||
jid: c.jid,
|
||||
@ -34,19 +39,32 @@ class DatabaseRepository {
|
||||
unreadCounter: c.unreadCounter,
|
||||
lastChangeTimestamp: c.lastChangeTimestamp,
|
||||
sharedMediaPaths: [],
|
||||
open: c.open,
|
||||
triggeredByDatabase: true
|
||||
open: c.open
|
||||
)
|
||||
));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadMessagesForJid(String jid) async {
|
||||
final messages = await this.isar.dBMessages.where().fromEqualTo(jid).findAll();
|
||||
this.loadedConversations.add(jid);
|
||||
|
||||
messages.forEach((m) => this.store.dispatch(AddMessageAction(message: Message(
|
||||
from: m.from,
|
||||
body: m.body,
|
||||
timestamp: m.timestamp,
|
||||
sent: m.sent,
|
||||
id: m.id!
|
||||
))));
|
||||
}
|
||||
|
||||
// TODO
|
||||
bool hasConversation(int id) {
|
||||
return this._cache.containsKey(id);
|
||||
}
|
||||
|
||||
Future<void> updateConversation({ required int id, String? lastMessageBody, int? lastChangeTimestamp, bool? open }) async {
|
||||
Future<void> updateConversation({ required int id, String? lastMessageBody, int? lastChangeTimestamp, bool? open, int? unreadCounter }) async {
|
||||
print("updateConversation");
|
||||
|
||||
final c = this._cache[id]!;
|
||||
@ -59,40 +77,64 @@ class DatabaseRepository {
|
||||
if (open != null) {
|
||||
c.open = open;
|
||||
}
|
||||
if (unreadCounter != null) {
|
||||
c.unreadCounter = unreadCounter;
|
||||
}
|
||||
|
||||
await this.isar.writeTxn((isar) async {
|
||||
await isar.conversations.put(c);
|
||||
await isar.dBConversations.put(c);
|
||||
print("DONE");
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addConversationFromAction(AddConversationAction action) async {
|
||||
print("addConversationFromACtion");
|
||||
final c = db.Conversation()
|
||||
..jid = action.jid
|
||||
..title = action.title
|
||||
..avatarUrl = action.avatarUrl
|
||||
..lastChangeTimestamp = action.lastChangeTimestamp
|
||||
..unreadCounter = action.unreadCounter
|
||||
..lastMessageBody = action.lastMessageBody
|
||||
..open = action.open;
|
||||
|
||||
Future<Conversation> addConversationFromData(String title, String lastMessageBody, String avatarUrl, String jid, int unreadCounter, int lastChangeTimestamp, List<String> sharedMediaPaths, bool open) async {
|
||||
print("addConversationFromAction");
|
||||
final c = DBConversation()
|
||||
..jid = jid
|
||||
..title = title
|
||||
..avatarUrl = avatarUrl
|
||||
..lastChangeTimestamp = lastChangeTimestamp
|
||||
..unreadCounter = unreadCounter
|
||||
..lastMessageBody = lastMessageBody
|
||||
..open = open;
|
||||
await this.isar.writeTxn((isar) async {
|
||||
await isar.conversations.put(c);
|
||||
await isar.dBConversations.put(c);
|
||||
print("DONE");
|
||||
});
|
||||
this._cache[c.id!] = c;
|
||||
|
||||
return Conversation(
|
||||
title: title,
|
||||
lastMessageBody: lastMessageBody,
|
||||
avatarUrl: avatarUrl,
|
||||
jid: jid,
|
||||
id: c.id!,
|
||||
unreadCounter: unreadCounter,
|
||||
lastChangeTimestamp: lastChangeTimestamp,
|
||||
sharedMediaPaths: sharedMediaPaths,
|
||||
open: open
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> addConversation(model.Conversation conversation) async {
|
||||
final c = db.Conversation()
|
||||
..jid = conversation.jid
|
||||
..title = conversation.title
|
||||
..avatarUrl = conversation.avatarUrl
|
||||
..lastChangeTimestamp = conversation.lastChangeTimestamp
|
||||
..unreadCounter = conversation.unreadCounter
|
||||
..lastMessageBody = conversation.lastMessageBody
|
||||
..open = conversation.open;
|
||||
|
||||
Future<Message> addMessageFromData(String body, int timestamp, String from, bool sent) async {
|
||||
print("addMessageFromData");
|
||||
final m = DBMessage()
|
||||
..from = from
|
||||
..timestamp = timestamp
|
||||
..body = body
|
||||
..sent = sent;
|
||||
|
||||
await this.isar.writeTxn((isar) async {
|
||||
await isar.conversations.put(c);
|
||||
await isar.dBMessages.put(m);
|
||||
print("DONE");
|
||||
});
|
||||
|
||||
return Message(
|
||||
body: body,
|
||||
from: from,
|
||||
timestamp: timestamp,
|
||||
sent: sent,
|
||||
id: m.id!
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -13,10 +13,13 @@ import "package:moxxyv2/ui/pages/profile/profile.dart";
|
||||
import "package:moxxyv2/ui/pages/conversation/arguments.dart";
|
||||
import "package:moxxyv2/ui/constants.dart";
|
||||
import "package:moxxyv2/ui/helpers.dart";
|
||||
import "package:moxxyv2/repositories/conversation.dart";
|
||||
import "package:moxxyv2/redux/messages/actions.dart";
|
||||
|
||||
import "package:flutter_speed_dial/flutter_speed_dial.dart";
|
||||
import "package:flutter_redux/flutter_redux.dart";
|
||||
import "package:redux/redux.dart";
|
||||
import "package:get_it/get_it.dart";
|
||||
|
||||
typedef SendMessageFunction = void Function(String body);
|
||||
|
||||
@ -50,8 +53,9 @@ class _MessageListViewModel {
|
||||
final void Function(bool scrollToEndButton) setShowScrollToEndButton;
|
||||
final bool showScrollToEndButton;
|
||||
final void Function() closeChat;
|
||||
|
||||
_MessageListViewModel({ required this.conversation, required this.showSendButton, required this.sendMessage, required this.setShowSendButton, required this.showScrollToEndButton, required this.setShowScrollToEndButton, required this.closeChat });
|
||||
final void Function(String) loadMessages;
|
||||
|
||||
_MessageListViewModel({ required this.conversation, required this.showSendButton, required this.sendMessage, required this.setShowSendButton, required this.showScrollToEndButton, required this.setShowScrollToEndButton, required this.closeChat, required this.loadMessages });
|
||||
}
|
||||
|
||||
class _ListViewWrapperViewModel {
|
||||
@ -182,6 +186,7 @@ class ConversationPage extends StatelessWidget {
|
||||
jid: jid,
|
||||
id: conversation.id
|
||||
)),
|
||||
loadMessages: (jid) => store.dispatch(LoadMessagesAction(jid: jid)),
|
||||
sendMessage: (body) => store.dispatch(
|
||||
// TODO
|
||||
SendMessageAction(
|
||||
@ -195,6 +200,11 @@ class ConversationPage extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
builder: (context, viewModel) {
|
||||
// TODO: Handle this in a middleware
|
||||
if (GetIt.I.get<DatabaseRepository>().loadedConversations.indexOf(jid) == -1) {
|
||||
viewModel.loadMessages(jid);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: BorderlessTopbar.avatarAndName(
|
||||
avatar: AvatarWrapper(
|
||||
|
@ -1,8 +1,10 @@
|
||||
import "package:moxxyv2/xmpp/jid.dart";
|
||||
|
||||
abstract class XmppEvent {}
|
||||
|
||||
class MessageEvent extends XmppEvent {
|
||||
final String body;
|
||||
final String fromJid;
|
||||
final FullJID fromJid;
|
||||
final String sid;
|
||||
|
||||
MessageEvent({ required this.body, required this.fromJid, required this.sid });
|
||||
|
@ -30,7 +30,17 @@ class BareJID extends JID {
|
||||
|
||||
class FullJID extends JID {
|
||||
FullJID({ required String local, required String domain, required String resource }) : super(local: local, domain: domain, resource: resource);
|
||||
|
||||
BareJID toBare() {
|
||||
return BareJID(local: this.local, domain: this.domain);
|
||||
}
|
||||
|
||||
static FullJID fromString(String fullJid) {
|
||||
final jidParts = fullJid.split("@");
|
||||
final other = jidParts[1].split("/");
|
||||
return FullJID(local: jidParts[0], domain: other[0], resource: other[1]);
|
||||
}
|
||||
|
||||
String toString() {
|
||||
return "${this.local}@${this.domain}/${this.resource}";
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import "package:moxxyv2/xmpp/stanzas/stanza.dart";
|
||||
import "package:moxxyv2/xmpp/connection.dart";
|
||||
import "package:moxxyv2/xmpp/events.dart";
|
||||
import "package:moxxyv2/xmpp/jid.dart";
|
||||
|
||||
bool handleMessageStanza(XmppConnection conn, Stanza stanza) {
|
||||
final body = stanza.firstTag("body");
|
||||
@ -8,7 +9,7 @@ bool handleMessageStanza(XmppConnection conn, Stanza stanza) {
|
||||
|
||||
conn.sendEvent(MessageEvent(
|
||||
body: body.innerText(),
|
||||
fromJid: stanza.attributes["from"]!,
|
||||
fromJid: FullJID.fromString(stanza.attributes["from"]!),
|
||||
sid: stanza.attributes["id"]!
|
||||
));
|
||||
|
||||
|
@ -18,12 +18,12 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group("listContains", () {
|
||||
group("firstWhereOrNull", () {
|
||||
test("[] should not contain 1", () {
|
||||
expect(listContains<int>([], (int element) => element == 1), false);
|
||||
expect(firstWhereOrNull<int>([], (int element) => element == 1), null);
|
||||
});
|
||||
test("[1, 2, 3] should contain 2", () {
|
||||
expect(listContains([ 1, 2, 3 ], (int element) => element == 2), true);
|
||||
expect(firstWhereOrNull([ 1, 2, 3 ], (int element) => element == 2), 2);
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user