Implemented Message Archive Management (XEP-313)

This commit is contained in:
ChronosX88 2019-05-26 11:43:58 +04:00
parent bd732fa825
commit 2f454ec05f
18 changed files with 521 additions and 122 deletions

View File

@ -2,11 +2,11 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 2, "version": 2,
"identityHash": "2409c873b47ccd635ed7d10e4d8604f8", "identityHash": "b15f54b6ea1393c5d45bb222f98ed7d6",
"entities": [ "entities": [
{ {
"tableName": "messages", "tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `jid` TEXT, `senderJid` TEXT, `timestamp` INTEGER NOT NULL, `text` TEXT, `isSent` INTEGER NOT NULL, `isRead` INTEGER NOT NULL)", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chatID` TEXT, `messageUid` TEXT, `senderJid` TEXT, `timestamp` INTEGER NOT NULL, `text` TEXT, `isSent` INTEGER NOT NULL, `isRead` INTEGER NOT NULL)",
"fields": [ "fields": [
{ {
"fieldPath": "messageID", "fieldPath": "messageID",
@ -15,8 +15,14 @@
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "jid", "fieldPath": "chatID",
"columnName": "jid", "columnName": "chatID",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "messageUid",
"columnName": "messageUid",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
}, },
@ -102,7 +108,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"2409c873b47ccd635ed7d10e4d8604f8\")" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b15f54b6ea1393c5d45bb222f98ed7d6\")"
] ]
} }
} }

View File

@ -0,0 +1,120 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "b9b5e578f9222b641c833e4ba5e2378c",
"entities": [
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chatID` TEXT, `messageUid` TEXT, `senderJid` TEXT, `timestamp` INTEGER NOT NULL, `text` TEXT, `isSent` INTEGER NOT NULL, `isRead` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "messageID",
"columnName": "messageID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chatID",
"columnName": "chatID",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "messageUid",
"columnName": "messageUid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "senderJid",
"columnName": "senderJid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isSent",
"columnName": "isSent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isRead",
"columnName": "isRead",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"messageID"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "chats",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jid` TEXT NOT NULL, `chatName` TEXT, `users` TEXT, `unreadMessagesCount` INTEGER NOT NULL, `lastMessageUid` TEXT, PRIMARY KEY(`jid`))",
"fields": [
{
"fieldPath": "jid",
"columnName": "jid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "chatName",
"columnName": "chatName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "users",
"columnName": "users",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "unreadMessagesCount",
"columnName": "unreadMessagesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessageUid",
"columnName": "lastMessageUid",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"jid"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b9b5e578f9222b641c833e4ba5e2378c\")"
]
}
}

View File

@ -0,0 +1,120 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "86a2e626209f2c93a3c961821c967d5c",
"entities": [
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageID` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chatID` TEXT, `messageUid` TEXT, `senderJid` TEXT, `timestamp` INTEGER NOT NULL, `text` TEXT, `isSent` INTEGER NOT NULL, `isRead` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "messageID",
"columnName": "messageID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "chatID",
"columnName": "chatID",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "messageUid",
"columnName": "messageUid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "senderJid",
"columnName": "senderJid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isSent",
"columnName": "isSent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isRead",
"columnName": "isRead",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"messageID"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "chats",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jid` TEXT NOT NULL, `chatName` TEXT, `users` TEXT, `unreadMessagesCount` INTEGER NOT NULL, `firstMessageUid` TEXT, PRIMARY KEY(`jid`))",
"fields": [
{
"fieldPath": "jid",
"columnName": "jid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "chatName",
"columnName": "chatName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "users",
"columnName": "users",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "unreadMessagesCount",
"columnName": "unreadMessagesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "firstMessageUid",
"columnName": "firstMessageUid",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"jid"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"86a2e626209f2c93a3c961821c967d5c\")"
]
}
}

View File

@ -72,7 +72,7 @@ public class XMPPConnection implements ConnectionListener {
public XMPPConnection(Context context) { public XMPPConnection(Context context) {
this.prefs = PreferenceManager.getDefaultSharedPreferences(context); this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
this.context = context; this.context = context;
String jid = prefs.getString("jid", null); String jid = prefs.getString("chatID", null);
String password = prefs.getString("pass", null); String password = prefs.getString("pass", null);
if(jid != null && password != null) { if(jid != null && password != null) {
String username = jid.split("@")[0]; String username = jid.split("@")[0];
@ -124,6 +124,8 @@ public class XMPPConnection implements ConnectionListener {
try { try {
if(mamManager.isSupported()) { if(mamManager.isSupported()) {
MamManager.getInstanceFor(connection).enableMamForAllMessages(); MamManager.getInstanceFor(connection).enableMamForAllMessages();
} else {
mamManager = null;
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
@ -170,17 +172,19 @@ public class XMPPConnection implements ConnectionListener {
e.printStackTrace(); e.printStackTrace();
} }
public void sendMessage(EntityBareJid recipientJid, String messageText) { public String sendMessage(EntityBareJid recipientJid, String messageText) {
Chat chat = ChatManager.getInstanceFor(connection).chatWith(recipientJid); Chat chat = ChatManager.getInstanceFor(connection).chatWith(recipientJid);
try { try {
Message message = new Message(recipientJid, Message.Type.chat); Message message = new Message(recipientJid, Message.Type.chat);
message.setBody(messageText); message.setBody(messageText);
chat.send(message); chat.send(message);
return message.getStanzaId();
} catch (SmackException.NotConnectedException e) { } catch (SmackException.NotConnectedException e) {
e.printStackTrace(); e.printStackTrace();
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
} }
return null;
} }
public XMPPTCPConnection getConnection() { public XMPPTCPConnection getConnection() {
@ -240,4 +244,11 @@ public class XMPPConnection implements ConnectionListener {
} }
} }
} }
public MamManager getMamManager() {
if(isConnectionAlive()) {
return mamManager;
}
return null;
}
} }

View File

@ -21,12 +21,13 @@ import android.content.Intent
import android.view.MenuItem import android.view.MenuItem
import com.stfalcon.chatkit.dialogs.DialogsListAdapter import com.stfalcon.chatkit.dialogs.DialogsListAdapter
import com.stfalcon.chatkit.messages.MessagesListAdapter import com.stfalcon.chatkit.messages.MessagesListAdapter
import io.github.chronosx88.influence.models.GenericDialog import io.github.chronosx88.influence.models.GenericDialog
import io.github.chronosx88.influence.models.GenericMessage import io.github.chronosx88.influence.models.GenericMessage
import io.github.chronosx88.influence.models.roomEntities.ChatEntity import io.github.chronosx88.influence.models.roomEntities.ChatEntity
import io.github.chronosx88.influence.models.roomEntities.MessageEntity import io.github.chronosx88.influence.models.roomEntities.MessageEntity
import java9.util.concurrent.CompletableFuture
import org.jivesoftware.smack.roster.RosterEntry import org.jivesoftware.smack.roster.RosterEntry
import org.jivesoftware.smackx.mam.MamManager
interface CoreContracts { interface CoreContracts {
@ -81,11 +82,16 @@ interface CoreContracts {
interface IChatLogicContract { interface IChatLogicContract {
fun sendMessage(text: String): MessageEntity? fun sendMessage(text: String): MessageEntity?
fun getUserStatus(): Boolean fun getUserStatus(): Boolean
fun loadMessagesFromMAM(): CompletableFuture<MamManager.MamQuery?>
fun loadRecentPageMessages(): CompletableFuture<MamManager.MamQuery?>
fun loadLocalMessages(): List<MessageEntity>?
} }
interface IChatPresenterContract { interface IChatPresenterContract {
fun sendMessage(text: String): Boolean fun sendMessage(text: String): Boolean
fun loadLocalMessages() fun loadLocalMessages()
fun loadMoreMessages()
fun loadRecentPageMessages()
fun onDestroy() fun onDestroy()
fun onOptionsItemSelected(item: MenuItem) fun onOptionsItemSelected(item: MenuItem)
} }

View File

@ -90,7 +90,7 @@ public class AppHelper extends MultiDexApplication {
private static void loadLoginCredentials() { private static void loadLoginCredentials() {
currentLoginCredentials = new LoginCredentials(); currentLoginCredentials = new LoginCredentials();
String jid = preferences.getString("jid", null); String jid = preferences.getString("chatID", null);
String password = preferences.getString("pass", null); String password = preferences.getString("pass", null);
if(jid != null && password != null) { if(jid != null && password != null) {
String username = jid.split("@")[0]; String username = jid.split("@")[0];
@ -104,7 +104,7 @@ public class AppHelper extends MultiDexApplication {
public static void resetLoginCredentials() { public static void resetLoginCredentials() {
currentLoginCredentials = new LoginCredentials(); currentLoginCredentials = new LoginCredentials();
preferences.edit().remove("jid").apply(); preferences.edit().remove("chatID").apply();
preferences.edit().remove("pass").apply(); preferences.edit().remove("pass").apply();
} }
@ -126,6 +126,7 @@ public class AppHelper extends MultiDexApplication {
private void initChatDB() { private void initChatDB() {
chatDB = Room.databaseBuilder(getApplicationContext(), RoomHelper.class, "chatDB") chatDB = Room.databaseBuilder(getApplicationContext(), RoomHelper.class, "chatDB")
.fallbackToDestructiveMigration() // FIXME ONLY FOR TEST ENVIRONMENT! DON'T USE THIS IN PRODUCTION!
.allowMainThreadQueries() .allowMainThreadQueries()
.build(); .build();
} }

View File

@ -29,16 +29,16 @@ public class LocalDBWrapper {
private static RoomHelper dbInstance = AppHelper.getChatDB(); private static RoomHelper dbInstance = AppHelper.getChatDB();
public static void createChatEntry(String jid, String chatName) { public static void createChatEntry(String jid, String chatName) {
dbInstance.chatDao().addChat(new ChatEntity(jid, chatName, new ArrayList<>(), 0)); dbInstance.chatDao().addChat(new ChatEntity(jid, chatName, new ArrayList<>(), 0, ""));
} }
public static long createMessageEntry(String jid, String senderJid, long timestamp, String text, boolean isSent, boolean isRead) { public static long createMessageEntry(String chatID, String messageUid, String senderJid, long timestamp, String text, boolean isSent, boolean isRead) {
List<ChatEntity> chatEntities = AppHelper.getChatDB().chatDao().getChatByChatID(jid); List<ChatEntity> chatEntities = AppHelper.getChatDB().chatDao().getChatByChatID(chatID);
if(chatEntities.size() < 1) { if(chatEntities.size() < 1) {
Log.e(LOG_TAG, "Failed to create message entry because chat " + jid + " doesn't exists!"); Log.e(LOG_TAG, "Failed to create message entry because chat " + chatID + " doesn't exists!");
return -1; return -1;
} }
MessageEntity message = new MessageEntity(jid, senderJid, timestamp, text, isSent, isRead); MessageEntity message = new MessageEntity(chatID, messageUid, senderJid, timestamp, text, isSent, isRead);
long index = dbInstance.messageDao().insertMessage(message); long index = dbInstance.messageDao().insertMessage(message);
return index; return index;
} }
@ -51,6 +51,14 @@ public class LocalDBWrapper {
return messages.get(0); return messages.get(0);
} }
public static MessageEntity getMessageByUID(String messageUID) {
List<MessageEntity> messages = dbInstance.messageDao().getMessageByUID(messageUID);
if(messages.isEmpty()) {
return null;
}
return messages.get(0);
}
public static List<MessageEntity> getMessagesByChatID(String chatID) { public static List<MessageEntity> getMessagesByChatID(String chatID) {
List<MessageEntity> messages = dbInstance.messageDao().getMessagesByChatID(chatID); List<MessageEntity> messages = dbInstance.messageDao().getMessagesByChatID(chatID);
if(messages.isEmpty()) { if(messages.isEmpty()) {
@ -85,6 +93,11 @@ public class LocalDBWrapper {
return getMessageByID(messageID); return getMessageByID(messageID);
} }
public static MessageEntity getFirstMessage(String chatID) {
long messageID = dbInstance.messageDao().getFirstMessageByChatID(chatID);
return getMessageByID(messageID);
}
public static void updateChatUnreadMessagesCount(String chatID, int unreadMessagesCount) { public static void updateChatUnreadMessagesCount(String chatID, int unreadMessagesCount) {
dbInstance.chatDao().updateUnreadMessagesCount(chatID, unreadMessagesCount); dbInstance.chatDao().updateUnreadMessagesCount(chatID, unreadMessagesCount);
} }

View File

@ -19,7 +19,6 @@ package io.github.chronosx88.influence.helpers;
import com.instacart.library.truetime.TrueTime; import com.instacart.library.truetime.TrueTime;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.jivesoftware.smack.PresenceListener;
import org.jivesoftware.smack.chat2.Chat; import org.jivesoftware.smack.chat2.Chat;
import org.jivesoftware.smack.chat2.IncomingChatMessageListener; import org.jivesoftware.smack.chat2.IncomingChatMessageListener;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message;
@ -44,7 +43,7 @@ public class NetworkHandler implements IncomingChatMessageListener, PresenceEven
if(LocalDBWrapper.getChatByChatID(from.asEntityBareJidString()) == null) { if(LocalDBWrapper.getChatByChatID(from.asEntityBareJidString()) == null) {
LocalDBWrapper.createChatEntry(chatID, chat.getXmppAddressOfChatPartner().asBareJid().asUnescapedString().split("@")[0]); LocalDBWrapper.createChatEntry(chatID, chat.getXmppAddressOfChatPartner().asBareJid().asUnescapedString().split("@")[0]);
} }
long messageID = LocalDBWrapper.createMessageEntry(chatID, from.asUnescapedString(), TrueTime.now().getTime(), message.getBody(), true, false); long messageID = LocalDBWrapper.createMessageEntry(chatID, message.getStanzaId(), from.asUnescapedString(), TrueTime.now().getTime(), message.getBody(), true, false);
int newUnreadMessagesCount = LocalDBWrapper.getChatByChatID(chatID).unreadMessagesCount + 1; int newUnreadMessagesCount = LocalDBWrapper.getChatByChatID(chatID).unreadMessagesCount + 1;
LocalDBWrapper.updateChatUnreadMessagesCount(chatID, newUnreadMessagesCount); LocalDBWrapper.updateChatUnreadMessagesCount(chatID, newUnreadMessagesCount);

View File

@ -24,7 +24,7 @@ import io.github.chronosx88.influence.models.daos.MessageDao;
import io.github.chronosx88.influence.models.roomEntities.ChatEntity; import io.github.chronosx88.influence.models.roomEntities.ChatEntity;
import io.github.chronosx88.influence.models.roomEntities.MessageEntity; import io.github.chronosx88.influence.models.roomEntities.MessageEntity;
@Database(entities = { MessageEntity.class, ChatEntity.class }, version = 2) @Database(entities = { MessageEntity.class, ChatEntity.class }, version = 4)
@TypeConverters({RoomTypeConverter.class}) @TypeConverters({RoomTypeConverter.class})
public abstract class RoomHelper extends RoomDatabase { public abstract class RoomHelper extends RoomDatabase {

View File

@ -1,84 +0,0 @@
/*
* Copyright 2019 ChronosX88
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.chronosx88.influence.logic;
import com.instacart.library.truetime.TrueTime;
import org.jivesoftware.smack.packet.Presence;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import java.io.IOException;
import io.github.chronosx88.influence.contracts.CoreContracts;
import io.github.chronosx88.influence.helpers.AppHelper;
import io.github.chronosx88.influence.helpers.LocalDBWrapper;
import io.github.chronosx88.influence.models.roomEntities.ChatEntity;
import io.github.chronosx88.influence.models.roomEntities.MessageEntity;
public class ChatLogic implements CoreContracts.IChatLogicContract {
private String chatID;
private ChatEntity chatEntity;
public ChatLogic(ChatEntity chatEntity) {
this.chatEntity = chatEntity;
this.chatID = chatEntity.jid;
}
@Override
public MessageEntity sendMessage(String text) {
if (AppHelper.getXmppConnection().isConnectionAlive()) {
EntityBareJid jid;
try {
jid = JidCreate.entityBareFrom(chatEntity.jid);
} catch (XmppStringprepException e) {
return null;
}
AppHelper.getXmppConnection().sendMessage(jid, text);
while (!TrueTime.isInitialized()) {
new Thread(() -> {
try {
TrueTime.build().initialize();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
long messageID = LocalDBWrapper.createMessageEntry(chatID, AppHelper.getJid(), TrueTime.now().getTime(), text, true, false);
return LocalDBWrapper.getMessageByID(messageID);
} else {
return null;
}
}
@Override
public boolean getUserStatus() {
if(AppHelper.getXmppConnection() != null) {
if(AppHelper.getXmppConnection().isConnectionAlive()) {
Presence presence = null;
try {
presence = AppHelper.getXmppConnection().getUserPresence(JidCreate.bareFrom(chatID));
} catch (XmppStringprepException e) {
e.printStackTrace();
}
return presence.isAvailable();
}
}
return false;
}
}

View File

@ -0,0 +1,136 @@
/*
* Copyright 2019 ChronosX88
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.chronosx88.influence.logic
import com.instacart.library.truetime.TrueTime
import org.jivesoftware.smack.packet.Presence
import org.jxmpp.jid.EntityBareJid
import org.jxmpp.jid.impl.JidCreate
import org.jxmpp.stringprep.XmppStringprepException
import java.io.IOException
import io.github.chronosx88.influence.contracts.CoreContracts
import io.github.chronosx88.influence.helpers.AppHelper
import io.github.chronosx88.influence.helpers.LocalDBWrapper
import io.github.chronosx88.influence.models.roomEntities.ChatEntity
import io.github.chronosx88.influence.models.roomEntities.MessageEntity
import java9.util.concurrent.CompletableFuture
import org.jivesoftware.smackx.mam.MamManager
class ChatLogic(private val chatEntity: ChatEntity) : CoreContracts.IChatLogicContract {
private val chatID: String
private var mamManager: MamManager? = null
init {
this.chatID = chatEntity.jid
}
override fun sendMessage(text: String): MessageEntity? {
if (AppHelper.getXmppConnection().isConnectionAlive) {
val jid: EntityBareJid
try {
jid = JidCreate.entityBareFrom(chatEntity.jid)
} catch (e: XmppStringprepException) {
return null
}
val messageUid = AppHelper.getXmppConnection().sendMessage(jid, text)
while (!TrueTime.isInitialized()) {
Thread {
try {
TrueTime.build().initialize()
} catch (e: IOException) {
e.printStackTrace()
}
}.start()
}
var timestamp: Long
try {
timestamp = TrueTime.now().time
} catch (e: Exception) {
// Fallback to Plain Old Java CurrentTimeMillis
timestamp = System.currentTimeMillis()
}
val messageID = LocalDBWrapper.createMessageEntry(chatID, messageUid, AppHelper.getJid(), timestamp, text, true, false)
return LocalDBWrapper.getMessageByID(messageID)
} else {
return null
}
}
override fun getUserStatus(): Boolean {
if (AppHelper.getXmppConnection() != null) {
if (AppHelper.getXmppConnection().isConnectionAlive) {
var presence: Presence? = null
try {
presence = AppHelper.getXmppConnection().getUserPresence(JidCreate.bareFrom(chatID))
} catch (e: XmppStringprepException) {
e.printStackTrace()
}
return presence!!.isAvailable
}
}
return false
}
override fun loadMessagesFromMAM(): CompletableFuture<MamManager.MamQuery?> {
return CompletableFuture.supplyAsync {
if(AppHelper.getXmppConnection() != null) {
val mamManager: MamManager? = AppHelper.getXmppConnection().mamManager
if(mamManager != null) {
val firstMessageUid = LocalDBWrapper.getChatByChatID(chatID).firstMessageUid
if(firstMessageUid != "") {
return@supplyAsync mamManager.queryArchive(MamManager.MamQueryArgs.builder()
.beforeUid(firstMessageUid)
.limitResultsToJid(JidCreate.from(chatID))
.setResultPageSizeTo(50)
.build())
} else {
return@supplyAsync null
}
} else {
return@supplyAsync null
}
} else {
return@supplyAsync null
}
}
}
override fun loadLocalMessages(): List<MessageEntity>? {
return LocalDBWrapper.getMessagesByChatID(chatID)
}
override fun loadRecentPageMessages(): CompletableFuture<MamManager.MamQuery?> {
return CompletableFuture.supplyAsync {
if(AppHelper.getXmppConnection() != null) {
val mamManager: MamManager? = AppHelper.getXmppConnection().mamManager
if(mamManager != null) {
return@supplyAsync mamManager.queryMostRecentPage(JidCreate.from(chatID), 20)
} else {
return@supplyAsync null
}
} else {
return@supplyAsync null
}
}
}
}

View File

@ -25,12 +25,14 @@ import io.github.chronosx88.influence.models.roomEntities.MessageEntity;
public class GenericMessage implements IMessage { public class GenericMessage implements IMessage {
private long messageID; private long messageID;
private String messageUid;
private IUser author; private IUser author;
private long timestamp; private long timestamp;
private String text; private String text;
public GenericMessage(MessageEntity messageEntity) { public GenericMessage(MessageEntity messageEntity) {
this.messageID = messageEntity.messageID; this.messageID = messageEntity.messageID;
this.messageUid = messageEntity.messageUid;
this.author = new GenericUser(messageEntity.senderJid, messageEntity.senderJid, messageEntity.senderJid); this.author = new GenericUser(messageEntity.senderJid, messageEntity.senderJid, messageEntity.senderJid);
this.timestamp = messageEntity.timestamp; this.timestamp = messageEntity.timestamp;
this.text = messageEntity.text; this.text = messageEntity.text;
@ -55,4 +57,8 @@ public class GenericMessage implements IMessage {
public Date getCreatedAt() { public Date getCreatedAt() {
return new Date(timestamp); return new Date(timestamp);
} }
public String getMessageUid() {
return messageUid;
}
} }

View File

@ -27,16 +27,16 @@ import io.github.chronosx88.influence.models.roomEntities.MessageEntity;
@Dao @Dao
public interface MessageDao { public interface MessageDao {
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert
long insertMessage(MessageEntity chatModel); long insertMessage(MessageEntity chatModel);
@Query("DELETE FROM messages WHERE messageID = :messageID") @Query("DELETE FROM messages WHERE messageID = :messageID")
void deleteMessage(String messageID); void deleteMessage(String messageID);
@Query("DELETE FROM messages WHERE jid = :jid") @Query("DELETE FROM messages WHERE chatID = :jid")
void deleteMessagesByChatID(String jid); void deleteMessagesByChatID(String jid);
@Query("SELECT * FROM messages WHERE jid = :jid") @Query("SELECT * FROM messages WHERE chatID = :jid")
List<MessageEntity> getMessagesByChatID(String jid); List<MessageEntity> getMessagesByChatID(String jid);
@Query("SELECT * FROM messages WHERE messageID = :messageID") @Query("SELECT * FROM messages WHERE messageID = :messageID")
@ -48,9 +48,15 @@ public interface MessageDao {
@Query("DELETE FROM messages") @Query("DELETE FROM messages")
void clearMessages(); void clearMessages();
@Query("DELETE FROM messages WHERE jid = :chatID") @Query("DELETE FROM messages WHERE chatID = :chatID")
void clearMessagesByChatID(String chatID); void clearMessagesByChatID(String chatID);
@Query("SELECT messageID FROM messages WHERE jid = :chatID GROUP BY :chatID HAVING MAX(messageID)") @Query("SELECT messageID FROM messages WHERE chatID = :chatID GROUP BY :chatID HAVING MAX(timestamp)")
long getLastMessageByChatID(String chatID); long getLastMessageByChatID(String chatID);
@Query("SELECT messageID FROM messages WHERE chatID = :chatID GROUP BY :chatID HAVING MIN(timestamp)")
long getFirstMessageByChatID(String chatID);
@Query("SELECT * FROM messages WHERE messageUid = :uid")
List<MessageEntity> getMessageByUID(String uid);
} }

View File

@ -31,15 +31,17 @@ public class ChatEntity {
@ColumnInfo public String chatName; @ColumnInfo public String chatName;
@ColumnInfo public ArrayList<GenericUser> users; @ColumnInfo public ArrayList<GenericUser> users;
@ColumnInfo public int unreadMessagesCount; @ColumnInfo public int unreadMessagesCount;
@ColumnInfo public String firstMessageUid;
public ChatEntity(@NonNull String jid, String chatName, ArrayList<GenericUser> users, int unreadMessagesCount) { public ChatEntity(@NonNull String jid, String chatName, ArrayList<GenericUser> users, int unreadMessagesCount, String firstMessageUid) {
this.jid = jid; this.jid = jid;
this.chatName = chatName; this.chatName = chatName;
this.users = users; this.users = users;
this.unreadMessagesCount = unreadMessagesCount; this.unreadMessagesCount = unreadMessagesCount;
this.firstMessageUid = firstMessageUid;
} }
public boolean isPrivateChat() { public boolean isPrivateChat() {
return users.size() == 1; return users.size() == 2;
} }
} }

View File

@ -24,15 +24,17 @@ import androidx.room.PrimaryKey;
@Entity(tableName = "messages") @Entity(tableName = "messages")
public class MessageEntity { public class MessageEntity {
@PrimaryKey(autoGenerate = true) public long messageID; // Global message ID @PrimaryKey(autoGenerate = true) public long messageID; // Global message ID
@ColumnInfo public String jid; // Chat ID @ColumnInfo public String chatID; // Chat ID
@ColumnInfo public String messageUid;
@ColumnInfo public String senderJid; @ColumnInfo public String senderJid;
@ColumnInfo public long timestamp; // Timestamp @ColumnInfo public long timestamp; // Timestamp
@ColumnInfo public String text; // Message text @ColumnInfo public String text; // Message text
@ColumnInfo public boolean isSent; // Send status indicator @ColumnInfo public boolean isSent; // Send status indicator
@ColumnInfo public boolean isRead; // Message Read Indicator @ColumnInfo public boolean isRead; // Message Read Indicator
public MessageEntity(String jid, String senderJid, long timestamp, String text, boolean isSent, boolean isRead) { public MessageEntity(String chatID, String messageUid, String senderJid, long timestamp, String text, boolean isSent, boolean isRead) {
this.jid = jid; this.chatID = chatID;
this.messageUid = messageUid;
this.senderJid = senderJid; this.senderJid = senderJid;
this.timestamp = timestamp; this.timestamp = timestamp;
this.text = text; this.text = text;

View File

@ -33,18 +33,21 @@ import io.github.chronosx88.influence.models.appEvents.UserPresenceChangedEvent
import io.github.chronosx88.influence.models.roomEntities.ChatEntity import io.github.chronosx88.influence.models.roomEntities.ChatEntity
import io.github.chronosx88.influence.models.roomEntities.MessageEntity import io.github.chronosx88.influence.models.roomEntities.MessageEntity
import java9.util.concurrent.CompletableFuture import java9.util.concurrent.CompletableFuture
import kotlinx.coroutines.GlobalScope import java9.util.stream.StreamSupport
import kotlinx.coroutines.async
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import kotlin.math.log import org.jivesoftware.smackx.forward.packet.Forwarded
import java.util.*
import kotlin.Comparator
import kotlin.collections.ArrayList
class ChatPresenter(private val view: CoreContracts.IChatViewContract, private val chatID: String) : CoreContracts.IChatPresenterContract { class ChatPresenter(private val view: CoreContracts.IChatViewContract, private val chatID: String) : CoreContracts.IChatPresenterContract {
private val logic: CoreContracts.IChatLogicContract private val logic: CoreContracts.IChatLogicContract
private val chatEntity: ChatEntity? private val chatEntity: ChatEntity?
private val gson: Gson private val gson: Gson
private val chatAdapter: MessagesListAdapter<GenericMessage> private val chatAdapter: MessagesListAdapter<GenericMessage>
private val messageComparator = Comparator<GenericMessage> { o1, o2 -> o1.createdAt.time.compareTo(o2.createdAt.time) }
init { init {
this.logic = ChatLogic(LocalDBWrapper.getChatByChatID(chatID)!!) this.logic = ChatLogic(LocalDBWrapper.getChatByChatID(chatID)!!)
@ -53,6 +56,7 @@ class ChatPresenter(private val view: CoreContracts.IChatViewContract, private v
val holdersConfig = MessageHolders() val holdersConfig = MessageHolders()
holdersConfig.setIncomingTextLayout(R.layout.item_incoming_text_message_custom) holdersConfig.setIncomingTextLayout(R.layout.item_incoming_text_message_custom)
chatAdapter = MessagesListAdapter(AppHelper.getJid(), holdersConfig, AvatarImageLoader()) chatAdapter = MessagesListAdapter(AppHelper.getJid(), holdersConfig, AvatarImageLoader())
chatAdapter.setLoadMoreListener { page, _ -> loadMoreMessages() }
view.setAdapter(chatAdapter) view.setAdapter(chatAdapter)
getUserStatus() getUserStatus()
EventBus.getDefault().register(this) EventBus.getDefault().register(this)
@ -70,13 +74,14 @@ class ChatPresenter(private val view: CoreContracts.IChatViewContract, private v
} }
override fun loadLocalMessages() { override fun loadLocalMessages() {
val entities: List<MessageEntity>? = LocalDBWrapper.getMessagesByChatID(chatID) val entities = logic.loadLocalMessages()
val messages = ArrayList<GenericMessage>() val messages = ArrayList<GenericMessage>()
if(entities != null) { if(entities != null) {
entities.forEach { entities.forEach {
messages.add(GenericMessage(it)) messages.add(GenericMessage(it))
} }
} }
messages.sortWith(messageComparator)
chatAdapter.addToEnd(messages, true) chatAdapter.addToEnd(messages, true)
} }
@ -123,4 +128,58 @@ class ChatPresenter(private val view: CoreContracts.IChatViewContract, private v
} }
} }
} }
override fun loadMoreMessages() {
logic.loadMessagesFromMAM().thenAccept { query ->
if(query != null) {
val adapterMessages = ArrayList<GenericMessage>()
StreamSupport.stream(query.page.forwarded)
.forEach { forwardedMessage ->
val message = Forwarded.extractMessagesFrom(Collections.singleton(forwardedMessage))[0]
if(message.body != null) {
if(LocalDBWrapper.getMessageByUID(message.stanzaId) == null) {
val messageID = LocalDBWrapper.createMessageEntry(chatID, message.stanzaId, message.from.asBareJid().asUnescapedString(), forwardedMessage.delayInformation.stamp.time, message.body, true, true)
adapterMessages.add(GenericMessage(LocalDBWrapper.getMessageByID(messageID)))
}
}
}
AppHelper.getMainUIThread().post {
adapterMessages.sortWith(messageComparator)
chatAdapter.addToEnd(adapterMessages, true)
}
if(query.messageCount != 0) {
chatEntity!!.firstMessageUid = query.mamResultExtensions[0].id
LocalDBWrapper.updateChatEntity(chatEntity)
}
}
}
}
override fun loadRecentPageMessages() {
logic.loadRecentPageMessages().thenAccept { query ->
if(query != null) {
val adapterMessages = ArrayList<GenericMessage>()
StreamSupport.stream(query.page.forwarded)
.forEach { forwardedMessage ->
val message = Forwarded.extractMessagesFrom(Collections.singleton(forwardedMessage))[0]
if(message.body != null) {
if(LocalDBWrapper.getMessageByUID(message.stanzaId) == null) {
val messageID = LocalDBWrapper.createMessageEntry(chatID, message.stanzaId, message.from.asBareJid().asUnescapedString(), forwardedMessage.delayInformation.stamp.time, message.body, true, true)
adapterMessages.add(GenericMessage(LocalDBWrapper.getMessageByID(messageID)))
}
}
}
AppHelper.getMainUIThread().post {
adapterMessages.sortWith(messageComparator)
adapterMessages.forEach {
chatAdapter.addToStart(it, true)
}
}
if(query.messageCount != 0 && chatEntity!!.firstMessageUid == "") {
chatEntity.firstMessageUid = query.mamResultExtensions[0].id
LocalDBWrapper.updateChatEntity(chatEntity)
}
}
}
}
} }

View File

@ -28,18 +28,13 @@ import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import com.stfalcon.chatkit.commons.ImageLoader
import com.stfalcon.chatkit.messages.MessageInput import com.stfalcon.chatkit.messages.MessageInput
import com.stfalcon.chatkit.messages.MessagesList import com.stfalcon.chatkit.messages.MessagesList
import com.stfalcon.chatkit.messages.MessagesListAdapter import com.stfalcon.chatkit.messages.MessagesListAdapter
import io.github.chronosx88.influence.R import io.github.chronosx88.influence.R
import io.github.chronosx88.influence.contracts.CoreContracts import io.github.chronosx88.influence.contracts.CoreContracts
import io.github.chronosx88.influence.helpers.AppHelper
import io.github.chronosx88.influence.helpers.LocalDBWrapper
import io.github.chronosx88.influence.models.GenericMessage import io.github.chronosx88.influence.models.GenericMessage
import io.github.chronosx88.influence.models.roomEntities.MessageEntity
import io.github.chronosx88.influence.presenters.ChatPresenter import io.github.chronosx88.influence.presenters.ChatPresenter
import kotlinx.android.synthetic.main.activity_chat.view.*
import org.jetbrains.anko.find import org.jetbrains.anko.find
class ChatActivity : AppCompatActivity(), CoreContracts.IChatViewContract { class ChatActivity : AppCompatActivity(), CoreContracts.IChatViewContract {
@ -74,6 +69,7 @@ class ChatActivity : AppCompatActivity(), CoreContracts.IChatViewContract {
presenter = ChatPresenter(this, intent.getStringExtra("chatID")) presenter = ChatPresenter(this, intent.getStringExtra("chatID"))
loadAvatarFromIntent(intent) loadAvatarFromIntent(intent)
presenter!!.loadLocalMessages() presenter!!.loadLocalMessages()
presenter!!.loadRecentPageMessages()
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {

View File

@ -131,7 +131,7 @@ public class LoginActivity extends AppCompatActivity implements CoreContracts.IL
private void saveLoginCredentials() { private void saveLoginCredentials() {
AppHelper.getPreferences().edit() AppHelper.getPreferences().edit()
.putString("jid", jidEditText.getText().toString()) .putString("chatID", jidEditText.getText().toString())
.putString("pass", passwordEditText.getText().toString()) .putString("pass", passwordEditText.getText().toString())
.putBoolean("logged_in", true) .putBoolean("logged_in", true)
.apply(); .apply();