Skip to content
Snippets Groups Projects
Commit a02538f2 authored by Pierre Nicolas's avatar Pierre Nicolas :joy:
Browse files

chatview: implement status mark for messages

This patch is only about swarm one:one

GitLab: #1620

Change-Id: I31438e0afa9541b004e1842c161238cceba19a2c
parent 0847d5a0
Branches
Tags
No related merge requests found
......@@ -318,27 +318,63 @@ class ConversationAdapter(
conversationViewHolder: ConversationViewHolder,
interaction: Interaction
) {
val statusIcon = conversationViewHolder.mStatusIcon ?: return
val messageToAttach = conversationViewHolder.mLayoutStatusIconId?.id ?: return
val conversation = interaction.conversation
if (conversation == null || conversation !is Conversation) {
conversationViewHolder.mStatusIcon?.isVisible = false
statusIcon.isVisible = false
return
}
conversationViewHolder.compositeDisposable.add(presenter.conversationFacade
// Attach the statusIcon to the message layout.
statusIcon.attachToMessage(messageToAttach)
// Remove user from statusMap.
val modifiedStatusMap = interaction.statusMap
.filter { !conversation.findContact(net.jami.model.Uri.fromId(it.key))!!.isUser }
val isDisplayed = modifiedStatusMap.any { it.value == Interaction.MessageStates.DISPLAYED }
val isSending = modifiedStatusMap.isEmpty() or
modifiedStatusMap.any { it.value == Interaction.MessageStates.SENDING }
val isReceived = modifiedStatusMap.any { it.value == Interaction.MessageStates.SUCCESS }
val lastDisplayedIdx = conversation.lastDisplayedMessages
.map { conversation.getMessage(it.value) }
.maxOfOrNull { mInteractions.indexOf(it) }
val currentIdx = mInteractions.indexOf(interaction)
// Case 1: Message is sending
if(!isDisplayed && isSending){
statusIcon.let {
it.visibility = View.VISIBLE
it.updateSending()
}
}
// Case 2: Message is received by at least one contact
else if(!isDisplayed && isReceived){
statusIcon.let {
it.visibility = View.VISIBLE
it.updateSuccess()
}
}
// Case 3: Message is displayed
else if (isDisplayed && currentIdx == lastDisplayedIdx) {
conversationViewHolder.compositeDisposable.add(
presenter.conversationFacade
.getLoadedContact(
interaction.account!!,
conversation,
interaction.displayedContacts
accountId = interaction.account!!,
conversation = conversation,
contactIds = modifiedStatusMap.map { it.key }
)
.observeOn(DeviceUtils.uiScheduler)
.subscribe { contacts ->
conversationViewHolder.mStatusIcon?.isVisible = contacts.isNotEmpty()
conversationViewHolder.mStatusIcon?.update(
contacts,
interaction.status,
conversationViewHolder.mLayoutStatusIconId?.id ?: View.NO_ID
)
.subscribe { seenBy ->
statusIcon.visibility = View.VISIBLE
statusIcon.updateDisplayed(seenBy)
})
}
// Case 4: Message is displayed but not the last displayed message
else statusIcon.visibility = View.GONE
}
/**
* Configure a reaction chip and logic about it.
......
......@@ -32,95 +32,117 @@ import net.jami.model.ContactViewModel
import net.jami.model.Interaction
import net.jami.utils.Log
/**
* MessageStatusView display the status of a message (sending, sent, displayed).
* Sending and sent Status are displayed as icons, while Displayed status is displayed as avatars.
*/
class MessageStatusView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : LinearLayout(context, attrs, defStyle) {
@IdRes
private var attachedMessage: Int = View.NO_ID
private val iconSize = resources.getDimensionPixelSize(R.dimen.conversation_status_icon_size)
private val iconTint: ColorStateList =
ColorStateList.valueOf(ContextCompat.getColor(context, R.color.grey_500))
// Add or remove views to match the given count.
// "Sending" or "Sent" need 1 view, "Displayed" needs as many views as there are contacts.
private fun resize(count: Int) {
if (count == 0) {
removeAllViews()
} else if (childCount > count) {
while (childCount > count) {
removeViewAt(childCount - 1)
}
} else if (childCount < count) {
var i = childCount
while (childCount < count) {
if (count == childCount) return
// Update layout only if there is a change in the mode (empty, single or multiple).
if (childCount == 0 || (count == 1 && childCount > 1) || (count > 1 && childCount == 1))
layout(count)
if (count == 0) removeAllViews()
else if (childCount > count) while (childCount > count) removeViewAt(childCount - 1)
else if (childCount < count) repeat(count - childCount) {
addView(ImageView(context).apply {
layoutParams = LayoutParams(iconSize, iconSize).apply {
marginStart = if (i != 0) -iconSize/3 else 0
marginStart = if (it != 0) -iconSize / 3 else 0
}
})
i++
}
}
}
fun update(contacts: Collection<ContactViewModel>, status: Interaction.InteractionStatus, @IdRes resId: Int = View.NO_ID) {
val showStatus = contacts.isEmpty() && (status == Interaction.InteractionStatus.SUCCESS || status == Interaction.InteractionStatus.SENDING)
if (showStatus) {
resize(1)
(getChildAt(0) as ImageView).apply {
setImageResource(when (status) {
Interaction.InteractionStatus.FAILURE -> R.drawable.round_highlight_off_24
Interaction.InteractionStatus.SENDING -> R.drawable.baseline_check_circle_outline_24
Interaction.InteractionStatus.SUCCESS -> R.drawable.baseline_check_circle_24
else -> -1 //R.drawable.baseline_check_circle_24
})
ImageViewCompat.setImageTintList(this, iconTint)
}
} else {
resize(contacts.size)
contacts.forEachIndexed { index, contact ->
(getChildAt(index) as ImageView).apply {
imageTintList = null
setImageDrawable(AvatarDrawable.Builder()
.withCircleCrop(true)
.withContact(contact)
.withPresence(false)
.build(context))
}
}
}
// Layout the views depending on the count and the layout type.
// If one view is displayed, it is put on the right of the message.
// If multiple views are displayed, they are put below the message.
private fun layout(count: Int) {
val fitRight = count < 2
when (layoutParams) {
is RelativeLayout.LayoutParams -> {
val params = layoutParams as RelativeLayout.LayoutParams? ?: return
val fitRight = showStatus || contacts.size < 2
if (fitRight) {
// Put the avatar on the right of the message if there is only one contact
// Put the view on the right of the message.
params.removeRule(RelativeLayout.BELOW)
params.addRule(RelativeLayout.ALIGN_BOTTOM, resId)
params.addRule(RelativeLayout.ALIGN_BOTTOM, attachedMessage)
} else {
// Put the avatars below the message if there are multiple contacts
// Put the view below the message.
params.removeRule(RelativeLayout.ALIGN_BOTTOM)
params.addRule(RelativeLayout.BELOW, resId)
params.addRule(RelativeLayout.BELOW, attachedMessage)
}
layoutParams = params
}
is ConstraintLayout.LayoutParams -> {
val params = layoutParams as ConstraintLayout.LayoutParams? ?: return
val fitRight = showStatus || contacts.size < 2
if (fitRight) {
// Put the avatar on the right of the message if there is only one contact
// Put the view on the right of the message.
params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
params.topToBottom = ConstraintLayout.LayoutParams.UNSET
} else {
// Put the avatars below the message if there are multiple contacts
// Put the view below the message.
params.bottomToBottom = ConstraintLayout.LayoutParams.UNSET
params.topToBottom = resId
params.topToBottom = attachedMessage
}
layoutParams = params
}
else -> Log.w(TAG, "Error layout params.")
}
}
fun attachToMessage(@IdRes resId: Int) {
attachedMessage = resId
layout(childCount)
}
fun updateSending() {
resize(1)
(getChildAt(0) as ImageView).apply {
setImageResource(R.drawable.sent)
ImageViewCompat.setImageTintList(this, iconTint)
}
}
fun updateSuccess() {
resize(1)
(getChildAt(0) as ImageView).apply {
setImageResource(R.drawable.receive)
ImageViewCompat.setImageTintList(this, iconTint)
}
}
fun updateDisplayed(seenBy: Collection<ContactViewModel>) {
resize(seenBy.size)
seenBy.forEachIndexed { index, contact ->
(getChildAt(index) as ImageView).apply {
imageTintList = null
setImageDrawable(
AvatarDrawable.Builder()
.withCircleCrop(true)
.withContact(contact)
.withPresence(false)
.build(context)
)
}
}
}
companion object {
val TAG = MessageStatusView::class.simpleName!!
}
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<group>
<clip-path
android:pathData="M0,0h12v12h-12z"/>
<path
android:pathData="M6.43,8.784 L3.007,5.362 4.06,4.309l2.37,2.37 4.314,-4.314A5.966,5.966 0,0 0,6 0c-0.032,0 -0.061,0.008 -0.094,0.01A5.98,5.98 0,0 0,0.094 5.074,5.911 5.911,0 0,0 0,6a5.911,5.911 0,0 0,0.094 0.926A5.98,5.98 0,0 0,5.906 11.99c0.032,0 0.061,0.01 0.094,0.01a6,6 0,0 0,5.533 -8.32Z"
android:fillColor="#60c880"/>
</group>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M6,6m-5.25,0a5.25,5.25 0,1 1,10.5 0a5.25,5.25 0,1 1,-10.5 0"
android:strokeAlpha="0.5"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#fff"
android:fillAlpha="0.5"/>
</vector>
......@@ -35,7 +35,7 @@ class Conversation : ConversationHistory {
private val currentCalls = ArrayList<Conference>()
val aggregateHistory = ArrayList<Interaction>(32)
private val lastDisplayedMessages: MutableMap<String, String> = HashMap()
val lastDisplayedMessages: MutableMap<String, String> = HashMap()
private val updatedElementSubject: Subject<Pair<Interaction, ElementStatus>> = PublishSubject.create()
private val clearedSubject: Subject<List<Interaction>> = PublishSubject.create()
private val callsSubject: Subject<List<Conference>> = BehaviorSubject.createDefault(emptyList())
......@@ -358,8 +358,20 @@ class Conversation : ConversationHistory {
updatedElementSubject.onNext(Pair(e, ElementStatus.UPDATE))
}
}
// Add contact to new displayed interaction
// Check if the new message is after the last displayed message (could be not the case).
val currentLastMessageDisplayed: Interaction? =
lastDisplayedMessages[contactId]?.let { getMessage(it) }
val newPotentialMessageDisplayed = getMessage(messageId)
val isAfter =
if (currentLastMessageDisplayed != null && newPotentialMessageDisplayed != null) {
isAfter(currentLastMessageDisplayed, newPotentialMessageDisplayed)
} else false
// Update the last displayed message
if (isAfter or (currentLastMessageDisplayed == null))
lastDisplayedMessages[contactId] = messageId
mMessages[messageId]?.let { e ->
e.displayedContacts.add(contactId)
updatedElementSubject.onNext(Pair(e, ElementStatus.UPDATE))
......@@ -367,18 +379,23 @@ class Conversation : ConversationHistory {
}
@Synchronized
fun updateSwarmInteraction(messageId: String, contactUri: Uri, newStatus: Interaction.InteractionStatus) {
val e = mMessages[messageId] ?: return
if (newStatus == Interaction.InteractionStatus.DISPLAYED) {
Log.w(TAG, "updateSwarmInteraction DISPLAYED")
fun updateSwarmInteraction(
messageId: String,
contactUri: Uri,
newStatus: Interaction.MessageStates,
) {
val interaction = mMessages[messageId] ?: return
if (newStatus == Interaction.MessageStates.DISPLAYED) {
findContact(contactUri)?.let { contact ->
if (!contact.isUser)
setLastMessageDisplayed(contactUri.host, messageId)
}
} else if (newStatus != Interaction.InteractionStatus.SENDING) {
e.status = newStatus
updatedElementSubject.onNext(Pair(e, ElementStatus.UPDATE))
} else if (newStatus != Interaction.MessageStates.SENDING) {
interaction.status = Interaction.InteractionStatus.SENDING
}
interaction.statusMap = interaction.statusMap.plus(Pair(contactUri.host, newStatus))
updatedElementSubject.onNext(Pair(interaction, ElementStatus.UPDATE))
}
@Synchronized
......
......@@ -36,6 +36,7 @@ open class Interaction {
var reactToId: String? = null
var reactions: MutableList<Interaction> = ArrayList()
var history: MutableList<Interaction> = ArrayList<Interaction>(1).apply { add(this@Interaction) }
var statusMap: Map<String, MessageStates> = emptyMap()
private var historySubject: Subject<List<Interaction>> = BehaviorSubject.createDefault(history)
......
......@@ -1016,18 +1016,27 @@ class AccountService(
incomingMessageSubject.onNext(Message(accountId, messageId, callId, from, messages))
}
fun accountMessageStatusChanged(accountId: String, conversationId: String, messageId: String, contactId: String, status: Int) {
val newStatus = InteractionStatus.fromIntTextMessage(status)
Log.d(TAG, "accountMessageStatusChanged: $accountId, $conversationId, $messageId, $contactId, $newStatus")
fun accountMessageStatusChanged(
accountId: String,
conversationId: String,
messageId: String,
contactId: String,
status: Int,
) {
val account = getAccount(accountId) ?: return
val interactionStatus = InteractionStatus.fromIntTextMessage(status)
val messageState = Interaction.MessageStates.fromInt(status)
if (conversationId.isEmpty() && !account.isJami) {
mHistoryService
.accountMessageStatusChanged(accountId, messageId, contactId, newStatus)
.subscribe({ t: TextMessage -> messageSubject.onNext(t) }) { e: Throwable ->
Log.e(TAG, "Error updating message: " + e.localizedMessage) }
.accountMessageStatusChanged(
accountId, messageId, contactId, interactionStatus, messageState
)
.blockingSubscribe({ t: TextMessage -> messageSubject.onNext(t) })
{ e: Throwable -> Log.e(TAG, "Error updating message: " + e.localizedMessage) }
} else {
account.getSwarm(conversationId)
?.updateSwarmInteraction(messageId, Uri.fromId(contactId), newStatus)
?.updateSwarmInteraction(messageId, Uri.fromId(contactId), messageState)
}
}
......@@ -1280,8 +1289,11 @@ class AccountService(
val interaction = getInteraction(account, conversation, body)
val edits = message.editions.map { getInteraction(account, conversation, it.toNative()) }
val reactions = message.reactions.map { getInteraction(account, conversation, it.toNative()) }
val statusMap = message.status.mapValues { Interaction.MessageStates.fromInt(it.value) }
interaction.addEdits(edits)
interaction.addReactions(reactions)
interaction.statusMap = statusMap
return interaction
}
......
......@@ -179,7 +179,13 @@ abstract class HistoryService {
txt
}.subscribeOn(scheduler)
fun accountMessageStatusChanged(accountId: String, daemonId: String, peer: String, status: InteractionStatus): Single<TextMessage> = Single.fromCallable {
fun accountMessageStatusChanged(
accountId: String,
daemonId: String,
peer: String,
interactionStatus: InteractionStatus,
messageState: Interaction.MessageStates,
): Single<TextMessage> = Single.fromCallable {
val textList = getInteractionDataDao(accountId).queryForEq(Interaction.COLUMN_DAEMON_ID, daemonId)
if (textList == null || textList.isEmpty()) {
throw RuntimeException("accountMessageStatusChanged: not able to find message with id $daemonId in database")
......@@ -190,7 +196,8 @@ abstract class HistoryService {
throw RuntimeException("accountMessageStatusChanged: received an invalid text message")
}
val msg = TextMessage(text)
msg.status = status
msg.status = interactionStatus
msg.statusMap = msg.statusMap.plus(accountId to messageState)
getInteractionDataDao(accountId).update(msg)
msg.account = accountId
msg
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment