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,
"database": {
"version": 2,
"identityHash": "2409c873b47ccd635ed7d10e4d8604f8",
"identityHash": "b15f54b6ea1393c5d45bb222f98ed7d6",
"entities": [
{
"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": [
{
"fieldPath": "messageID",
@ -15,8 +15,14 @@
"notNull": true
},
{
"fieldPath": "jid",
"columnName": "jid",
"fieldPath": "chatID",
"columnName": "chatID",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "messageUid",
"columnName": "messageUid",
"affinity": "TEXT",
"notNull": false
},
@ -102,7 +108,7 @@
"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, \"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) {
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
this.context = context;
String jid = prefs.getString("jid", null);
String jid = prefs.getString("chatID", null);
String password = prefs.getString("pass", null);
if(jid != null && password != null) {
String username = jid.split("@")[0];
@ -124,6 +124,8 @@ public class XMPPConnection implements ConnectionListener {
try {
if(mamManager.isSupported()) {
MamManager.getInstanceFor(connection).enableMamForAllMessages();
} else {
mamManager = null;
}
} catch (InterruptedException e) {
e.printStackTrace();
@ -170,17 +172,19 @@ public class XMPPConnection implements ConnectionListener {
e.printStackTrace();
}
public void sendMessage(EntityBareJid recipientJid, String messageText) {
public String sendMessage(EntityBareJid recipientJid, String messageText) {
Chat chat = ChatManager.getInstanceFor(connection).chatWith(recipientJid);
try {
Message message = new Message(recipientJid, Message.Type.chat);
message.setBody(messageText);
chat.send(message);
return message.getStanzaId();
} catch (SmackException.NotConnectedException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
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 com.stfalcon.chatkit.dialogs.DialogsListAdapter
import com.stfalcon.chatkit.messages.MessagesListAdapter
import io.github.chronosx88.influence.models.GenericDialog
import io.github.chronosx88.influence.models.GenericMessage
import io.github.chronosx88.influence.models.roomEntities.ChatEntity
import io.github.chronosx88.influence.models.roomEntities.MessageEntity
import java9.util.concurrent.CompletableFuture
import org.jivesoftware.smack.roster.RosterEntry
import org.jivesoftware.smackx.mam.MamManager
interface CoreContracts {
@ -81,11 +82,16 @@ interface CoreContracts {
interface IChatLogicContract {
fun sendMessage(text: String): MessageEntity?
fun getUserStatus(): Boolean
fun loadMessagesFromMAM(): CompletableFuture<MamManager.MamQuery?>
fun loadRecentPageMessages(): CompletableFuture<MamManager.MamQuery?>
fun loadLocalMessages(): List<MessageEntity>?
}
interface IChatPresenterContract {
fun sendMessage(text: String): Boolean
fun loadLocalMessages()
fun loadMoreMessages()
fun loadRecentPageMessages()
fun onDestroy()
fun onOptionsItemSelected(item: MenuItem)
}

View File

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

View File

@ -29,16 +29,16 @@ public class LocalDBWrapper {
private static RoomHelper dbInstance = AppHelper.getChatDB();
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) {
List<ChatEntity> chatEntities = AppHelper.getChatDB().chatDao().getChatByChatID(jid);
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(chatID);
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;
}
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);
return index;
}
@ -51,6 +51,14 @@ public class LocalDBWrapper {
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) {
List<MessageEntity> messages = dbInstance.messageDao().getMessagesByChatID(chatID);
if(messages.isEmpty()) {
@ -85,6 +93,11 @@ public class LocalDBWrapper {
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) {
dbInstance.chatDao().updateUnreadMessagesCount(chatID, unreadMessagesCount);
}

View File

@ -19,7 +19,6 @@ package io.github.chronosx88.influence.helpers;
import com.instacart.library.truetime.TrueTime;
import org.greenrobot.eventbus.EventBus;
import org.jivesoftware.smack.PresenceListener;
import org.jivesoftware.smack.chat2.Chat;
import org.jivesoftware.smack.chat2.IncomingChatMessageListener;
import org.jivesoftware.smack.packet.Message;
@ -44,7 +43,7 @@ public class NetworkHandler implements IncomingChatMessageListener, PresenceEven
if(LocalDBWrapper.getChatByChatID(from.asEntityBareJidString()) == null) {
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;
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.MessageEntity;
@Database(entities = { MessageEntity.class, ChatEntity.class }, version = 2)
@Database(entities = { MessageEntity.class, ChatEntity.class }, version = 4)
@TypeConverters({RoomTypeConverter.class})
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 {
private long messageID;
private String messageUid;
private IUser author;
private long timestamp;
private String text;
public GenericMessage(MessageEntity messageEntity) {
this.messageID = messageEntity.messageID;
this.messageUid = messageEntity.messageUid;
this.author = new GenericUser(messageEntity.senderJid, messageEntity.senderJid, messageEntity.senderJid);
this.timestamp = messageEntity.timestamp;
this.text = messageEntity.text;
@ -55,4 +57,8 @@ public class GenericMessage implements IMessage {
public Date getCreatedAt() {
return new Date(timestamp);
}
public String getMessageUid() {
return messageUid;
}
}

View File

@ -27,16 +27,16 @@ import io.github.chronosx88.influence.models.roomEntities.MessageEntity;
@Dao
public interface MessageDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
@Insert
long insertMessage(MessageEntity chatModel);
@Query("DELETE FROM messages WHERE messageID = :messageID")
void deleteMessage(String messageID);
@Query("DELETE FROM messages WHERE jid = :jid")
@Query("DELETE FROM messages WHERE chatID = :jid")
void deleteMessagesByChatID(String jid);
@Query("SELECT * FROM messages WHERE jid = :jid")
@Query("SELECT * FROM messages WHERE chatID = :jid")
List<MessageEntity> getMessagesByChatID(String jid);
@Query("SELECT * FROM messages WHERE messageID = :messageID")
@ -48,9 +48,15 @@ public interface MessageDao {
@Query("DELETE FROM messages")
void clearMessages();
@Query("DELETE FROM messages WHERE jid = :chatID")
@Query("DELETE FROM messages WHERE chatID = :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);
@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 ArrayList<GenericUser> users;
@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.chatName = chatName;
this.users = users;
this.unreadMessagesCount = unreadMessagesCount;
this.firstMessageUid = firstMessageUid;
}
public boolean isPrivateChat() {
return users.size() == 1;
return users.size() == 2;
}
}

View File

@ -24,15 +24,17 @@ import androidx.room.PrimaryKey;
@Entity(tableName = "messages")
public class MessageEntity {
@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 long timestamp; // Timestamp
@ColumnInfo public String text; // Message text
@ColumnInfo public boolean isSent; // Send status indicator
@ColumnInfo public boolean isRead; // Message Read Indicator
public MessageEntity(String jid, String senderJid, long timestamp, String text, boolean isSent, boolean isRead) {
this.jid = jid;
public MessageEntity(String chatID, String messageUid, String senderJid, long timestamp, String text, boolean isSent, boolean isRead) {
this.chatID = chatID;
this.messageUid = messageUid;
this.senderJid = senderJid;
this.timestamp = timestamp;
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.MessageEntity
import java9.util.concurrent.CompletableFuture
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import java9.util.stream.StreamSupport
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
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 {
private val logic: CoreContracts.IChatLogicContract
private val chatEntity: ChatEntity?
private val gson: Gson
private val chatAdapter: MessagesListAdapter<GenericMessage>
private val messageComparator = Comparator<GenericMessage> { o1, o2 -> o1.createdAt.time.compareTo(o2.createdAt.time) }
init {
this.logic = ChatLogic(LocalDBWrapper.getChatByChatID(chatID)!!)
@ -53,6 +56,7 @@ class ChatPresenter(private val view: CoreContracts.IChatViewContract, private v
val holdersConfig = MessageHolders()
holdersConfig.setIncomingTextLayout(R.layout.item_incoming_text_message_custom)
chatAdapter = MessagesListAdapter(AppHelper.getJid(), holdersConfig, AvatarImageLoader())
chatAdapter.setLoadMoreListener { page, _ -> loadMoreMessages() }
view.setAdapter(chatAdapter)
getUserStatus()
EventBus.getDefault().register(this)
@ -70,13 +74,14 @@ class ChatPresenter(private val view: CoreContracts.IChatViewContract, private v
}
override fun loadLocalMessages() {
val entities: List<MessageEntity>? = LocalDBWrapper.getMessagesByChatID(chatID)
val entities = logic.loadLocalMessages()
val messages = ArrayList<GenericMessage>()
if(entities != null) {
entities.forEach {
messages.add(GenericMessage(it))
}
}
messages.sortWith(messageComparator)
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 com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import com.stfalcon.chatkit.commons.ImageLoader
import com.stfalcon.chatkit.messages.MessageInput
import com.stfalcon.chatkit.messages.MessagesList
import com.stfalcon.chatkit.messages.MessagesListAdapter
import io.github.chronosx88.influence.R
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.roomEntities.MessageEntity
import io.github.chronosx88.influence.presenters.ChatPresenter
import kotlinx.android.synthetic.main.activity_chat.view.*
import org.jetbrains.anko.find
class ChatActivity : AppCompatActivity(), CoreContracts.IChatViewContract {
@ -74,6 +69,7 @@ class ChatActivity : AppCompatActivity(), CoreContracts.IChatViewContract {
presenter = ChatPresenter(this, intent.getStringExtra("chatID"))
loadAvatarFromIntent(intent)
presenter!!.loadLocalMessages()
presenter!!.loadRecentPageMessages()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View File

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