Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature : Show conversation history - part 1 - Show conversation history in chat screen #233

Merged
merged 19 commits into from Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -2,13 +2,16 @@ package com.wire.android.feature.conversation.content.datasources.local

import com.wire.android.InstrumentationTest
import com.wire.android.core.storage.db.user.UserDatabase
import com.wire.android.feature.contact.datasources.local.ContactDao
import com.wire.android.feature.contact.datasources.local.ContactEntity
import com.wire.android.feature.conversation.data.local.ConversationDao
import com.wire.android.feature.conversation.data.local.ConversationEntity
import com.wire.android.framework.storage.db.DatabaseTestRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeInstanceOf
import org.junit.Before
import org.junit.Rule
import org.junit.Test
Expand All @@ -20,15 +23,18 @@ class MessageDaoTest : InstrumentationTest() {
val databaseTestRule = DatabaseTestRule.create<UserDatabase>(appContext)

private lateinit var messageDao: MessageDao
private lateinit var contactDao: ContactDao
private lateinit var conversationDao: ConversationDao

private lateinit var conversationEntity: ConversationEntity
private lateinit var messageEntity: MessageEntity
private lateinit var contactEntity: ContactEntity

@Before
fun setUp() {
val userDatabase = databaseTestRule.database
messageDao = userDatabase.messageDao()
contactDao = userDatabase.contactDao()
conversationDao = userDatabase.conversationDao()

conversationEntity = ConversationEntity(TEST_CONVERSATION_ID, TEST_CONVERSATION_NAME, TEST_CONVERSATION_TYPE)
Expand All @@ -41,9 +47,11 @@ class MessageDaoTest : InstrumentationTest() {
state = TEST_MESSAGE_STATE,
time = TEST_MESSAGE_TIME
)
contactEntity = ContactEntity(TEST_USER_ID, TEST_CONTACT_NAME, TEST_CONTACT_ASSET_KEY)

runBlocking {
conversationDao.insert(conversationEntity)
contactDao.insert(contactEntity)
messageDao.insert(messageEntity)
}
}
Expand All @@ -53,8 +61,12 @@ class MessageDaoTest : InstrumentationTest() {
runBlocking {
val result = messageDao.messagesByConversationId(TEST_CONVERSATION_ID)

result.first().size shouldBeEqualTo 1
result.first().first() shouldBeEqualTo messageEntity
with(result.first()) {
size shouldBeEqualTo 1
first() shouldBeInstanceOf CombinedMessageContactEntity::class
messageEntity shouldBeEqualTo messageEntity
contactEntity shouldBeEqualTo contactEntity
ohassine marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

Expand Down Expand Up @@ -88,5 +100,7 @@ class MessageDaoTest : InstrumentationTest() {
private const val TEST_MESSAGE_CONTENT = "message-content"
private const val TEST_MESSAGE_STATE = "message-state"
private const val TEST_MESSAGE_TIME = "message-time"
private const val TEST_CONTACT_NAME = "contact-name"
private const val TEST_CONTACT_ASSET_KEY = "contact-asset-key"
}
}
Expand Up @@ -29,7 +29,7 @@ val eventModule = module {
return scarlet.create()
}
//TODO hardcoded client to be replaced with current clientId
single { WebSocketConfig("cac5f0abcafab91e") }
single { WebSocketConfig("7d86fca5842c6d29") }
single {
provideWebSocketService(
get(),
Expand Down
Expand Up @@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Delete

@Dao
interface ContactDao {
Expand Down
Expand Up @@ -22,7 +22,7 @@ class ContactMapper(private val assetMapper: AssetMapper) {
fun fromContactEntityList(entityList: List<ContactEntity>): List<Contact> =
entityList.map { fromContactEntity(it) }

private fun fromContactEntity(entity: ContactEntity): Contact =
fun fromContactEntity(entity: ContactEntity): Contact =
Contact(
id = entity.id,
name = entity.name,
Expand Down
@@ -1,8 +1,9 @@
package com.wire.android.feature.conversation.content

import com.wire.android.feature.conversation.content.ui.CombinedMessageContact
import kotlinx.coroutines.flow.Flow

interface MessageRepository {
suspend fun decryptMessage(message: Message)
suspend fun conversationMessages(conversationId: String): Flow<List<Message>>
suspend fun conversationMessages(conversationId: String): Flow<List<CombinedMessageContact>>
}
Expand Up @@ -4,10 +4,12 @@ import android.util.Base64
import com.wire.android.core.crypto.CryptoBoxClient
import com.wire.android.core.exception.Failure
import com.wire.android.core.functional.Either
import com.wire.android.feature.contact.datasources.mapper.ContactMapper
import com.wire.android.feature.conversation.content.Message
import com.wire.android.feature.conversation.content.MessageRepository
import com.wire.android.feature.conversation.content.datasources.local.MessageLocalDataSource
import com.wire.android.feature.conversation.content.mapper.MessageMapper
import com.wire.android.feature.conversation.content.ui.CombinedMessageContact
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
Expand All @@ -17,6 +19,7 @@ import kotlinx.coroutines.launch
class MessageDataSource(
private val messageLocalDataSource: MessageLocalDataSource,
private val messageMapper: MessageMapper,
private val contactMapper: ContactMapper,
private val cryptoBoxClient: CryptoBoxClient
) : MessageRepository {

Expand All @@ -42,8 +45,13 @@ class MessageDataSource(
return messageLocalDataSource.save(messageEntity)
}

override suspend fun conversationMessages(conversationId: String): Flow<List<Message>> =
messageLocalDataSource.messagesByConversationId(conversationId).map { messages ->
messages.map { messageMapper.fromEntityToMessage(it) }
override suspend fun conversationMessages(conversationId: String): Flow<List<CombinedMessageContact>> =
messageLocalDataSource.messagesByConversationId(conversationId).map { messagesWithContact ->
messagesWithContact.map {
CombinedMessageContact(
messageMapper.fromEntityToMessage(it.messageEntity),
contactMapper.fromContactEntity(it.contactEntity)
)
}
}
}
@@ -0,0 +1,15 @@
package com.wire.android.feature.conversation.content.datasources.local

import androidx.room.Embedded
import androidx.room.Relation
import com.wire.android.feature.contact.datasources.local.ContactEntity

class CombinedMessageContactEntity(
@Embedded
val messageEntity: MessageEntity,
@Relation(
parentColumn = "sender_user_id",
entityColumn = "id"
)
val contactEntity: ContactEntity
)
Expand Up @@ -13,5 +13,5 @@ interface MessageDao {
suspend fun insert(message: MessageEntity)

@Query("SELECT * from message where conversation_id = :conversationId")
fun messagesByConversationId(conversationId: String): Flow<List<MessageEntity>>
fun messagesByConversationId(conversationId: String): Flow<List<CombinedMessageContactEntity>>
}
Expand Up @@ -8,6 +8,6 @@ import kotlinx.coroutines.flow.Flow
class MessageLocalDataSource(private val messageDao: MessageDao) : DatabaseService {
suspend fun save(message: MessageEntity): Either<Failure, Unit> = request { messageDao.insert(message) }

fun messagesByConversationId(conversationId: String): Flow<List<MessageEntity>> =
fun messagesByConversationId(conversationId: String): Flow<List<CombinedMessageContactEntity>> =
messageDao.messagesByConversationId(conversationId)
}
@@ -0,0 +1,6 @@
package com.wire.android.feature.conversation.content.ui

import com.wire.android.feature.contact.Contact
import com.wire.android.feature.conversation.content.Message

data class CombinedMessageContact(val message: Message, val contact: Contact)
Expand Up @@ -6,15 +6,23 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.wire.android.R
import kotlinx.android.synthetic.main.activity_conversation.*
import org.koin.android.viewmodel.ext.android.viewModel

class ConversationActivity : AppCompatActivity(R.layout.activity_conversation) {

private val conversationId get() = intent.getStringExtra(ARG_CONVERSATION_ID)
private val conversationTitle get() = intent.getStringExtra(ARG_CONVERSATION_TITLE)
private val viewModel by viewModel<ConversationViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUpConversationTitle()
setUpBackNavigation()
cacheConversationId()
}

private fun cacheConversationId() {
conversationId?.let { viewModel.cacheConversationId(it) }
}

private fun setUpBackNavigation() {
Expand Down
@@ -0,0 +1,48 @@
package com.wire.android.feature.conversation.content.ui

import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.wire.android.core.ui.recyclerview.ViewHolderInflater

class ConversationAdapter(private val viewHolderInflater: ViewHolderInflater) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private var messages: List<Any> = ArrayList()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
ConversationTextMessageViewHolder(parent, viewHolderInflater)

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (getItemViewType(position) == VIEW_TYPE_TEXT_MESSAGE) {
val shouldShowAvatar = shouldShowAvatar(position)
(holder as ConversationTextMessageViewHolder).bind(
(messages[position] as CombinedMessageContact),
shouldShowAvatar
)
}
}

override fun getItemViewType(position: Int): Int {
return when (messages[position]) {
is CombinedMessageContact -> VIEW_TYPE_TEXT_MESSAGE
else -> VIEW_TYPE_UNKNOWN
}
}

override fun getItemCount(): Int = messages.size

fun setList(newItems: List<Any>) {
this.messages = newItems
notifyDataSetChanged()
}

private fun shouldShowAvatar(position: Int): Boolean {
val currentMessage = (messages[position] as CombinedMessageContact).message
return (position == 0) ||
(position > 0 && currentMessage.senderUserId != (messages[position - 1] as CombinedMessageContact).message.senderUserId)
}

companion object {
const val VIEW_TYPE_TEXT_MESSAGE = 10
const val VIEW_TYPE_UNKNOWN = -1
}
}
@@ -1,7 +1,43 @@
package com.wire.android.feature.conversation.content.ui

import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.wire.android.R
import kotlinx.android.synthetic.main.fragment_conversation.*
import org.koin.android.ext.android.inject

//TODO to implement in next PR
class ConversationFragment : Fragment(R.layout.fragment_conversation)
class ConversationFragment : Fragment(R.layout.fragment_conversation) {

private val viewModel by activityViewModels<ConversationViewModel>()
private val conversationListAdapter by inject<ConversationAdapter>()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpRecycler(view)
observeConversationId()
observeMessages()
}

private fun observeConversationId() {
viewModel.conversationIdLiveData.observe(viewLifecycleOwner) {
viewModel.fetchMessages(it)
}
}

private fun setUpRecycler(view: View) {
val recyclerView: RecyclerView = view.findViewById(R.id.conversationRecyclerView)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = conversationListAdapter
}

private fun observeMessages() {
viewModel.conversationMessagesLiveData.observe(viewLifecycleOwner) {
conversationRecyclerView.scrollToPosition(it.size - 1);
conversationListAdapter.setList(it)
}
}
}
@@ -0,0 +1,28 @@
package com.wire.android.feature.conversation.content.ui

import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.imageview.ShapeableImageView
import com.wire.android.R
import com.wire.android.core.extension.lazyFind
import com.wire.android.core.ui.recyclerview.ViewHolderInflater

class ConversationTextMessageViewHolder(parent: ViewGroup, inflater: ViewHolderInflater) :
RecyclerView.ViewHolder(inflater.inflate(R.layout.conversation_chat_item_text, parent)) {

private val conversationChatItemUsernameTextView by lazyFind<TextView>(R.id.conversationChatItemUsernameTextView)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can get rid of those 3 lines and call directly conversationChatItemUsernameTextView , conversationChatItemTextMessageTextView and conversationChatItemUserAvatarImageView

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kotlin Synthetics is deprecated, I think we should use findViewById or migrate to Jetpack View Binding.
About migration:
https://developer.android.com/topic/libraries/view-binding/migration

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do the migration later in different PRs ;)

private val conversationChatItemTextMessageTextView by lazyFind<TextView>(R.id.conversationChatItemTextMessageTextView)
private val conversationChatItemUserAvatarImageView by lazyFind<ShapeableImageView>(R.id.conversationChatItemUserAvatarImageView)

fun bind(combinedMessage: CombinedMessageContact, shouldShowAvatar: Boolean) {
if (shouldShowAvatar)
conversationChatItemUserAvatarImageView.visibility = View.VISIBLE
else
conversationChatItemUserAvatarImageView.visibility = View.GONE

conversationChatItemUsernameTextView.text = combinedMessage.contact.name
conversationChatItemTextMessageTextView.text = combinedMessage.message.content
}
}
@@ -0,0 +1,34 @@
package com.wire.android.feature.conversation.content.ui

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.core.async.DispatcherProvider
import com.wire.android.core.usecase.DefaultUseCaseExecutor
import com.wire.android.core.usecase.UseCaseExecutor
import com.wire.android.feature.conversation.content.usecase.GetConversationUseCase
import com.wire.android.feature.conversation.content.usecase.GetConversationUseCaseParams

class ConversationViewModel(
override val dispatcherProvider: DispatcherProvider,
private val getConversationUseCase: GetConversationUseCase
) : ViewModel(), UseCaseExecutor by DefaultUseCaseExecutor(dispatcherProvider) {

private val _conversationIdLiveData: MutableLiveData<String> = MutableLiveData()
val conversationIdLiveData: LiveData<String> = _conversationIdLiveData

private val _conversationMessagesLiveData = MutableLiveData<List<CombinedMessageContact>>()
val conversationMessagesLiveData: LiveData<List<CombinedMessageContact>> = _conversationMessagesLiveData

fun cacheConversationId(conversationId: String) {
_conversationIdLiveData.value = conversationId
}

fun fetchMessages(conversationId: String) {
val params = GetConversationUseCaseParams(conversationId = conversationId)
getConversationUseCase(viewModelScope, params) {
_conversationMessagesLiveData.value = it
}
}
}
@@ -0,0 +1,15 @@
package com.wire.android.feature.conversation.content.usecase

import com.wire.android.core.usecase.ObservableUseCase
import com.wire.android.feature.conversation.content.MessageRepository
import com.wire.android.feature.conversation.content.ui.CombinedMessageContact
import kotlinx.coroutines.flow.Flow

class GetConversationUseCase(private val messageRepository: MessageRepository) :
ObservableUseCase<List<CombinedMessageContact>, GetConversationUseCaseParams> {

override suspend fun run(params: GetConversationUseCaseParams): Flow<List<CombinedMessageContact>> =
messageRepository.conversationMessages(params.conversationId)
}

data class GetConversationUseCaseParams(val conversationId: String)