improve send status handling

replace sendingFailed with SendStatus

add auto migration (incl deleting of column sendingFailed)

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2025-06-17 17:11:39 +02:00
parent 8c066eb521
commit 86bfaa8657
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
16 changed files with 49 additions and 75 deletions

View File

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 17, "version": 17,
"identityHash": "c6e75dfc4f897469a82de6aeff6208c1", "identityHash": "5bc4247e179307faa995552da5d34324",
"entities": [ "entities": [
{ {
"tableName": "User", "tableName": "User",
@ -445,7 +445,7 @@
}, },
{ {
"tableName": "ChatMessages", "tableName": "ChatMessages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendingFailed` INTEGER NOT NULL, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`internalId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `token` TEXT NOT NULL, `id` INTEGER NOT NULL, `internalConversationId` TEXT NOT NULL, `actorDisplayName` TEXT NOT NULL, `message` TEXT NOT NULL, `actorId` TEXT NOT NULL, `actorType` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `expirationTimestamp` INTEGER NOT NULL, `isReplyable` INTEGER NOT NULL, `isTemporary` INTEGER NOT NULL, `lastEditActorDisplayName` TEXT, `lastEditActorId` TEXT, `lastEditActorType` TEXT, `lastEditTimestamp` INTEGER, `markdown` INTEGER, `messageParameters` TEXT, `messageType` TEXT NOT NULL, `parent` INTEGER, `reactions` TEXT, `reactionsSelf` TEXT, `referenceId` TEXT, `sendStatus` TEXT, `silent` INTEGER NOT NULL, `systemMessage` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`internalId`), FOREIGN KEY(`internalConversationId`) REFERENCES `Conversations`(`internalId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [ "fields": [
{ {
"fieldPath": "internalId", "fieldPath": "internalId",
@ -581,12 +581,6 @@
"columnName": "referenceId", "columnName": "referenceId",
"affinity": "TEXT" "affinity": "TEXT"
}, },
{
"fieldPath": "sendingFailed",
"columnName": "sendingFailed",
"affinity": "INTEGER",
"notNull": true
},
{ {
"fieldPath": "sendStatus", "fieldPath": "sendStatus",
"columnName": "sendStatus", "columnName": "sendStatus",
@ -730,7 +724,7 @@
], ],
"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, 'c6e75dfc4f897469a82de6aeff6208c1')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bc4247e179307faa995552da5d34324')"
] ]
} }
} }

View File

@ -29,6 +29,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA
import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.ChatMessageRepository import com.nextcloud.talk.chat.data.ChatMessageRepository
import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding
@ -184,7 +185,7 @@ class OutcomingTextMessageViewHolder(itemView: View) :
binding.checkMark.visibility = View.INVISIBLE binding.checkMark.visibility = View.INVISIBLE
binding.sendingProgress.visibility = View.GONE binding.sendingProgress.visibility = View.GONE
if (message.sendingFailed) { if (message.sendStatus == SendStatus.FAILED) {
updateStatus(R.drawable.baseline_error_outline_24, context.resources?.getString(R.string.nc_message_failed)) updateStatus(R.drawable.baseline_error_outline_24, context.resources?.getString(R.string.nc_message_failed))
} else if (message.isTemporary) { } else if (message.isTemporary) {
updateStatus(R.drawable.baseline_schedule_24, context.resources?.getString(R.string.nc_message_sending)) updateStatus(R.drawable.baseline_schedule_24, context.resources?.getString(R.string.nc_message_sending))

View File

@ -200,7 +200,7 @@ class MessageInputFragment : Fragment() {
val connectionGained = (!wasOnline && isOnline) val connectionGained = (!wasOnline && isOnline)
Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained") Log.d(TAG, "isOnline: $isOnline\nwasOnline: $wasOnline\nconnectionGained: $connectionGained")
if (connectionGained) { if (connectionGained) {
chatActivity.messageInputViewModel.sendTempMessages( chatActivity.messageInputViewModel.sendUnsentMessages(
chatActivity.conversationUser!!.getCredentials(), chatActivity.conversationUser!!.getCredentials(),
ApiUtils.getUrlForChat( ApiUtils.getUrlForChat(
chatActivity.chatApiVersion, chatActivity.chatApiVersion,

View File

@ -110,7 +110,7 @@ interface ChatMessageRepository : LifecycleAwareManager {
suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow<Boolean> suspend fun editTempChatMessage(message: ChatMessage, editedMessageText: String): Flow<Boolean>
suspend fun sendTempChatMessages(credentials: String, url: String) suspend fun sendUnsentChatMessages(credentials: String, url: String)
suspend fun deleteTempMessage(chatMessage: ChatMessage) suspend fun deleteTempMessage(chatMessage: ChatMessage)
} }

View File

@ -14,6 +14,7 @@ import android.util.Log
import com.bluelinelabs.logansquare.annotation.JsonIgnore import com.bluelinelabs.logansquare.annotation.JsonIgnore
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage
import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.models.json.chat.ReadStatus
@ -119,7 +120,7 @@ data class ChatMessage(
var referenceId: String? = null, var referenceId: String? = null,
var sendingFailed: Boolean = true, var sendStatus: SendStatus? = null,
var silent: Boolean = false var silent: Boolean = false

View File

@ -215,7 +215,8 @@ class OfflineFirstChatRepository @Inject constructor(
) )
} }
sendTempChatMessages(credentials, urlForChatting) // this call could be deleted when we have a worker to send messages..
sendUnsentChatMessages(credentials, urlForChatting)
// delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing // delay is a dirty workaround to make sure messages are added to adapter on initial load before dealing
// with them (otherwise there is a race condition). // with them (otherwise there is a race condition).
@ -845,8 +846,6 @@ class OfflineFirstChatRepository @Inject constructor(
} }
} }
Log.d(TAG, "sending chat message: " + message)
return flow { return flow {
val response = network.sendChatMessage( val response = network.sendChatMessage(
credentials, credentials,
@ -881,7 +880,6 @@ class OfflineFirstChatRepository @Inject constructor(
referenceId referenceId
).firstOrNull() ).firstOrNull()
failedMessage?.let { failedMessage?.let {
// it.sendingFailed = true
it.sendStatus = SendStatus.FAILED it.sendStatus = SendStatus.FAILED
chatDao.updateChatMessage(it) chatDao.updateChatMessage(it)
@ -903,7 +901,6 @@ class OfflineFirstChatRepository @Inject constructor(
referenceId: String referenceId: String
): Flow<Result<ChatMessage?>> { ): Flow<Result<ChatMessage?>> {
val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first() val messageToResend = chatDao.getTempMessageForConversation(internalConversationId, referenceId).first()
// messageToResend.sendingFailed = false
messageToResend.sendStatus = SendStatus.PENDING messageToResend.sendStatus = SendStatus.PENDING
chatDao.updateChatMessage(messageToResend) chatDao.updateChatMessage(messageToResend)
@ -960,8 +957,8 @@ class OfflineFirstChatRepository @Inject constructor(
} }
} }
override suspend fun sendTempChatMessages(credentials: String, url: String) { override suspend fun sendUnsentChatMessages(credentials: String, url: String) {
val tempMessages = chatDao.getPendingOrFailedMessagesForConversation(internalConversationId).first() val tempMessages = chatDao.getTempUnsentMessagesForConversation(internalConversationId).first()
tempMessages.sortedBy { it.internalId }.onEach { tempMessages.sortedBy { it.internalId }.onEach {
sendChatMessage( sendChatMessage(
credentials, credentials,
@ -1055,7 +1052,7 @@ class OfflineFirstChatRepository @Inject constructor(
actorDisplayName = currentUser.displayName!!, actorDisplayName = currentUser.displayName!!,
referenceId = referenceId, referenceId = referenceId,
isTemporary = true, isTemporary = true,
sendingFailed = false, sendStatus = SendStatus.PENDING,
silent = sendWithoutNotification silent = sendWithoutNotification
) )
return entity return entity

View File

@ -169,9 +169,9 @@ class MessageInputViewModel @Inject constructor(
} }
} }
fun sendTempMessages(credentials: String, url: String) { fun sendUnsentMessages(credentials: String, url: String) {
viewModelScope.launch { viewModelScope.launch {
chatRepository.sendTempChatMessages( chatRepository.sendUnsentChatMessages(
credentials, credentials,
url url
) )

View File

@ -56,22 +56,11 @@ interface ChatMessagesDao {
FROM ChatMessages FROM ChatMessages
WHERE internalConversationId = :internalConversationId WHERE internalConversationId = :internalConversationId
AND isTemporary = 1 AND isTemporary = 1
AND (sendStatus = 'PENDING' OR sendStatus = 'FAILED') AND sendStatus != 'SENT_PENDING_ACK'
ORDER BY timestamp DESC, id DESC ORDER BY timestamp DESC, id DESC
""" """
) )
fun getPendingOrFailedMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>>
@Query(
"""
SELECT *
FROM ChatMessages
WHERE internalConversationId = :internalConversationId
AND (isTemporary = 1 OR sendStatus = 'SENT_PENDING_ACK')
ORDER BY timestamp DESC, id DESC
"""
)
fun getTempOrSendingAckMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>>
@Query( @Query(
""" """

View File

@ -68,7 +68,7 @@ fun ChatMessageEntity.asModel() =
isDeleted = deleted, isDeleted = deleted,
referenceId = referenceId, referenceId = referenceId,
isTemporary = isTemporary, isTemporary = isTemporary,
sendingFailed = sendingFailed, sendStatus = sendStatus,
readStatus = ReadStatus.NONE, readStatus = ReadStatus.NONE,
silent = silent silent = silent
) )

View File

@ -64,7 +64,6 @@ data class ChatMessageEntity(
@ColumnInfo(name = "reactions") var reactions: LinkedHashMap<String, Int>? = null, @ColumnInfo(name = "reactions") var reactions: LinkedHashMap<String, Int>? = null,
@ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList<String>? = null, @ColumnInfo(name = "reactionsSelf") var reactionsSelf: ArrayList<String>? = null,
@ColumnInfo(name = "referenceId") var referenceId: String? = null, @ColumnInfo(name = "referenceId") var referenceId: String? = null,
@ColumnInfo(name = "sendingFailed") var sendingFailed: Boolean = false,
@ColumnInfo(name = "sendStatus") var sendStatus: SendStatus? = null, @ColumnInfo(name = "sendStatus") var sendStatus: SendStatus? = null,
@ColumnInfo(name = "silent") var silent: Boolean = false, @ColumnInfo(name = "silent") var silent: Boolean = false,
@ColumnInfo(name = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType, @ColumnInfo(name = "systemMessage") var systemMessageType: ChatMessage.SystemMessageType,

View File

@ -1,7 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com> * SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */

View File

@ -1,18 +1,31 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2024-2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de> * SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
package com.nextcloud.talk.data.source.local package com.nextcloud.talk.data.source.local
import android.util.Log import android.util.Log
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import java.sql.SQLException import java.sql.SQLException
@Suppress("MagicNumber") @Suppress("MagicNumber")
object Migrations { object Migrations {
//region Auto migrations
@DeleteColumn(tableName = "ChatMessages", columnName = "sendingFailed")
class AutoMigration16To17 : AutoMigrationSpec
//endregion
//region Manual migrations
val MIGRATION_6_8 = object : Migration(6, 8) { val MIGRATION_6_8 = object : Migration(6, 8) {
override fun migrate(db: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 6 to 8") Log.i("Migrations", "Migrating 6 to 8")
@ -76,12 +89,7 @@ object Migrations {
} }
} }
val MIGRATION_16_17 = object : Migration(16, 17) { //endregion
override fun migrate(db: SupportSQLiteDatabase) {
Log.i("Migrations", "Migrating 16 to 17")
addSendStatus(db)
}
}
fun migrateToRoom(db: SupportSQLiteDatabase) { fun migrateToRoom(db: SupportSQLiteDatabase) {
db.execSQL( db.execSQL(
@ -326,17 +334,6 @@ object Migrations {
} }
} }
private fun addSendStatus(db: SupportSQLiteDatabase) {
try {
db.execSQL(
"ALTER TABLE ChatMessages " +
"ADD COLUMN sendStatus TEXT NOT NULL DEFAULT 'PENDING'"
)
} catch (e: SQLException) {
Log.i("Migrations", "Something went wrong when adding column sendStatus to table ChatMessages")
}
}
fun addTempMessagesSupport(db: SupportSQLiteDatabase) { fun addTempMessagesSupport(db: SupportSQLiteDatabase) {
try { try {
db.execSQL( db.execSQL(

View File

@ -1,7 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2023-2024 Marcel Hibbe <dev@mhibbe.de> * SPDX-FileCopyrightText: 2023-2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de> * SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017-2020 Mario Danic <mario@lovelyhq.com> * SPDX-FileCopyrightText: 2017-2020 Mario Danic <mario@lovelyhq.com>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
@ -23,6 +23,7 @@ import com.nextcloud.talk.data.database.dao.ConversationsDao
import com.nextcloud.talk.data.database.model.ChatBlockEntity import com.nextcloud.talk.data.database.model.ChatBlockEntity
import com.nextcloud.talk.data.database.model.ChatMessageEntity import com.nextcloud.talk.data.database.model.ChatMessageEntity
import com.nextcloud.talk.data.database.model.ConversationEntity import com.nextcloud.talk.data.database.model.ConversationEntity
import com.nextcloud.talk.data.source.local.Migrations.AutoMigration16To17
import com.nextcloud.talk.data.source.local.converters.ArrayListConverter import com.nextcloud.talk.data.source.local.converters.ArrayListConverter
import com.nextcloud.talk.data.source.local.converters.CapabilitiesConverter import com.nextcloud.talk.data.source.local.converters.CapabilitiesConverter
import com.nextcloud.talk.data.source.local.converters.ExternalSignalingServerConverter import com.nextcloud.talk.data.source.local.converters.ExternalSignalingServerConverter
@ -52,7 +53,8 @@ import java.util.Locale
], ],
version = 17, version = 17,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 9, to = 10) AutoMigration(from = 9, to = 10),
AutoMigration(from = 16, to = 17, spec = AutoMigration16To17::class)
], ],
exportSchema = true exportSchema = true
) )
@ -110,7 +112,7 @@ abstract class TalkDatabase : RoomDatabase() {
return Room return Room
.databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName) .databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName)
// comment out openHelperFactory to view the database entries in Android Studio for debugging // comment out openHelperFactory to view the database entries in Android Studio for debugging
// .openHelperFactory(factory) .openHelperFactory(factory)
.addMigrations( .addMigrations(
Migrations.MIGRATION_6_8, Migrations.MIGRATION_6_8,
Migrations.MIGRATION_7_8, Migrations.MIGRATION_7_8,
@ -120,8 +122,7 @@ abstract class TalkDatabase : RoomDatabase() {
Migrations.MIGRATION_12_13, Migrations.MIGRATION_12_13,
Migrations.MIGRATION_13_14, Migrations.MIGRATION_13_14,
Migrations.MIGRATION_14_15, Migrations.MIGRATION_14_15,
Migrations.MIGRATION_15_16, Migrations.MIGRATION_15_16
Migrations.MIGRATION_16_17
) )
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback( .addCallback(

View File

@ -1,7 +1,7 @@
/* /*
* Nextcloud Talk - Android Client * Nextcloud Talk - Android Client
* *
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com> * SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */

View File

@ -18,6 +18,7 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.chat.data.model.ChatMessage
import com.nextcloud.talk.data.database.model.SendStatus
import com.nextcloud.talk.data.network.NetworkMonitor import com.nextcloud.talk.data.network.NetworkMonitor
import com.nextcloud.talk.databinding.DialogTempMessageActionsBinding import com.nextcloud.talk.databinding.DialogTempMessageActionsBinding
import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.ui.theme.ViewThemeUtils
@ -58,9 +59,10 @@ class TempMessageActionsDialog(
private fun initMenuItems() { private fun initMenuItems() {
this.lifecycleScope.launch { this.lifecycleScope.launch {
initResendMessage(message.sendingFailed && networkMonitor.isOnline.value) val sendingFailed = message.sendStatus == SendStatus.FAILED
initMenuEditMessage(message.sendingFailed || !networkMonitor.isOnline.value) initResendMessage(sendingFailed && networkMonitor.isOnline.value)
initMenuDeleteMessage(message.sendingFailed || !networkMonitor.isOnline.value) initMenuEditMessage(sendingFailed || !networkMonitor.isOnline.value)
initMenuDeleteMessage(sendingFailed || !networkMonitor.isOnline.value)
initMenuItemCopy() initMenuItemCopy()
} }
} }

View File

@ -30,16 +30,9 @@ class DummyChatMessagesDaoImpl : ChatMessagesDao {
override fun getTempMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> = override fun getTempMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> =
flowOf() flowOf()
override fun getPendingOrFailedMessagesForConversation( override fun getTempUnsentMessagesForConversation(internalConversationId: String): Flow<List<ChatMessageEntity>> {
internalConversationId: String // nothing to return here as long this class is only used for the Search window
): Flow<List<ChatMessageEntity>> { return flowOf()
TODO("Not yet implemented")
}
override fun getTempOrSendingAckMessagesForConversation(
internalConversationId: String
): Flow<List<ChatMessageEntity>> {
TODO("Not yet implemented")
} }
override fun getTempMessageForConversation( override fun getTempMessageForConversation(