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 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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 @@ -42,8 +48,15 @@ class MessageDaoTest : InstrumentationTest() {
time = TEST_MESSAGE_TIME
)

contactEntity = ContactEntity(
id = TEST_USER_ID,
name = TEST_CONTACT_NAME,
assetKey = TEST_CONTACT_ASSET_KEY
)

runBlocking {
conversationDao.insert(conversationEntity)
contactDao.insert(contactEntity)
messageDao.insert(messageEntity)
}
}
Expand All @@ -53,8 +66,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 MessageAndContactEntity::class
messageEntity shouldBeEqualTo messageEntity
contactEntity shouldBeEqualTo contactEntity
ohassine marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

Expand Down Expand Up @@ -88,5 +105,8 @@ 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"
}
}
Original file line number Diff line number Diff line change
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("5d5f22a8b7a38acf") }
single {
provideWebSocketService(
get(),
Expand Down
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.wire.android.feature.conversation.content

import com.wire.android.feature.conversation.content.ui.MessageAndContact
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<MessageAndContact>>
}
Original file line number Diff line number Diff line change
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.MessageAndContact
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<MessageAndContact>> =
messageLocalDataSource.messagesByConversationId(conversationId).map { messagesWithContact ->
messagesWithContact.map {
MessageAndContact(
messageMapper.fromEntityToMessage(it.messageEntity),
contactMapper.fromContactEntity(it.contactEntity)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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 MessageAndContactEntity(
ohassine marked this conversation as resolved.
Show resolved Hide resolved
@Embedded
val messageEntity: MessageEntity,
@Relation(
parentColumn = "sender_user_id",
entityColumn = "id"
)
val contactEntity: ContactEntity
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction

import kotlinx.coroutines.flow.Flow

@Dao
Expand All @@ -13,5 +15,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<MessageAndContactEntity>>
}
Original file line number Diff line number Diff line change
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<MessageAndContactEntity>> =
messageDao.messagesByConversationId(conversationId)
}
Original file line number Diff line number Diff line change
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() {
viewModel.conversationId.value = conversationId
ohassine marked this conversation as resolved.
Show resolved Hide resolved
}

private fun setUpBackNavigation() {
Expand All @@ -33,6 +41,7 @@ class ConversationActivity : AppCompatActivity(R.layout.activity_conversation) {
putExtra(ARG_CONVERSATION_ID, conversationId)
putExtra(ARG_CONVERSATION_TITLE, conversationTitle)
}

private const val ARG_CONVERSATION_ID = "conversation-id-arg"
private const val ARG_CONVERSATION_TITLE = "conversation-title-arg"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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 {
return 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 MessageAndContact),
shouldShowAvatar
)
}
}

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

override fun getItemCount(): Int = messages.size

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

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

companion object {
private const val VIEW_TYPE_TEXT_MESSAGE = 10
private const val VIEW_TYPE_UNKNOWN = -1
}
}
Original file line number Diff line number Diff line change
@@ -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.conversationId.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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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(message: MessageAndContact, shouldShowAvatar: Boolean) {
if (shouldShowAvatar)
conversationChatItemUserAvatarImageView.visibility = View.VISIBLE
else
conversationChatItemUserAvatarImageView.visibility = View.GONE

conversationChatItemUsernameTextView.text = message.contact.name
conversationChatItemTextMessageTextView.text = message.message.content
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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) {

val conversationId: MutableLiveData<String> = MutableLiveData()
ohassine marked this conversation as resolved.
Show resolved Hide resolved
private val _conversationMessagesLiveData = MutableLiveData<List<MessageAndContact>>()
val conversationMessagesLiveData: LiveData<List<MessageAndContact>> = _conversationMessagesLiveData

fun fetchMessages(conversationId: String) {
val params = GetConversationUseCaseParams(conversationId = conversationId)
getConversationUseCase(viewModelScope, params) {
_conversationMessagesLiveData.value = it
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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 MessageAndContact(
val message: Message,
val contact: Contact
)
Original file line number Diff line number Diff line change
@@ -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.MessageAndContact
import kotlinx.coroutines.flow.Flow

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

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

data class GetConversationUseCaseParams(val conversationId: String)
Loading