mirror of
https://github.com/hmalik144/MessengerApp.git
synced 2025-12-10 03:05:28 +00:00
Creation of UI to display message and to send messages.
- Recycler view created - layout for message created - Two way messaging created with random message trigger after message submission - Messages are stored in room database and persisted there
This commit is contained in:
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -10,7 +10,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.example.h_mal.messengerapp"
|
applicationId "com.example.h_mal.messengerapp"
|
||||||
minSdkVersion 23
|
minSdkVersion 24
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
@@ -42,6 +42,8 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
||||||
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||||
|
implementation "androidx.cardview:cardview:1.0.0"
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class AppClass: Application(), KodeinAware{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
bind() from singleton { AppRoomDatabase(instance()) }
|
bind() from singleton { AppRoomDatabase(instance()) }
|
||||||
bind() from singleton { RepositoryImpl(instance()) }
|
bind() from singleton { RepositoryImpl(instance(), instance()) }
|
||||||
bind() from provider { MainViewModelFactory(instance()) }
|
bind() from provider { MainViewModelFactory(instance()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.h_mal.messengerapp.data.network
|
package com.example.h_mal.messengerapp.data.network
|
||||||
|
|
||||||
|
import com.example.h_mal.messengerapp.data.network.model.MessageItem
|
||||||
import com.tinder.scarlet.Scarlet
|
import com.tinder.scarlet.Scarlet
|
||||||
import com.tinder.scarlet.Stream
|
import com.tinder.scarlet.Stream
|
||||||
import com.tinder.scarlet.WebSocket
|
import com.tinder.scarlet.WebSocket
|
||||||
@@ -14,14 +15,16 @@ import okhttp3.OkHttpClient
|
|||||||
|
|
||||||
interface MessengerApi {
|
interface MessengerApi {
|
||||||
|
|
||||||
|
// Receive websocket messages in the form of string
|
||||||
@Receive
|
@Receive
|
||||||
fun observerMessage(): ReceiveChannel<String>
|
fun observerMessage(): ReceiveChannel<MessageItem>
|
||||||
|
|
||||||
@Receive
|
@Receive
|
||||||
fun observerFailure(): ReceiveChannel<WebSocket.Event.OnConnectionFailed>
|
fun observerEvent(): ReceiveChannel<WebSocket.Event>
|
||||||
|
|
||||||
|
// Send message to websocket and return pass/fail boolean result
|
||||||
@Send
|
@Send
|
||||||
fun send(message: Any): Boolean
|
fun send(message: MessageItem): Boolean
|
||||||
|
|
||||||
|
|
||||||
// invoke method creating an invocation of the api call
|
// invoke method creating an invocation of the api call
|
||||||
@@ -32,7 +35,7 @@ interface MessengerApi {
|
|||||||
|
|
||||||
val webSocketUrl = "ws://echo.websocket.org/"
|
val webSocketUrl = "ws://echo.websocket.org/"
|
||||||
|
|
||||||
// creation of retrofit class
|
// creation of Api class for websocket
|
||||||
return Scarlet.Builder()
|
return Scarlet.Builder()
|
||||||
.webSocketFactory(okkHttpClient.newWebSocketFactory(webSocketUrl))
|
.webSocketFactory(okkHttpClient.newWebSocketFactory(webSocketUrl))
|
||||||
.addMessageAdapterFactory(GsonMessageAdapter.Factory())
|
.addMessageAdapterFactory(GsonMessageAdapter.Factory())
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.example.h_mal.messengerapp.data.network.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.Expose
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class MessageItem(
|
||||||
|
@SerializedName("Message")
|
||||||
|
@Expose
|
||||||
|
var message: String? = null,
|
||||||
|
@SerializedName("IsSender")
|
||||||
|
@Expose
|
||||||
|
var isSender: Boolean? = null,
|
||||||
|
@SerializedName("TimeStamp")
|
||||||
|
@Expose
|
||||||
|
var timeStamp: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
package com.example.h_mal.messengerapp.data.repository
|
package com.example.h_mal.messengerapp.data.repository
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import com.example.h_mal.messengerapp.data.network.model.MessageItem
|
||||||
|
import com.example.h_mal.messengerapp.data.room.MessageEntity
|
||||||
import com.tinder.scarlet.WebSocket
|
import com.tinder.scarlet.WebSocket
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
|
|
||||||
interface Repository {
|
interface Repository {
|
||||||
|
|
||||||
fun observeText(): ReceiveChannel<String>
|
|
||||||
fun submitMessage(message: String)
|
fun observeWebsocketMessage(): ReceiveChannel<MessageItem>
|
||||||
fun getEvent(): ReceiveChannel<WebSocket.Event>
|
fun observeWebsocketEvent(): ReceiveChannel<WebSocket.Event>
|
||||||
|
fun submitMessageToApi(message: String, isSender: Boolean)
|
||||||
|
fun getMessagesLiveData(): LiveData<List<MessageEntity>>
|
||||||
|
suspend fun addMessageItemToDatabase(messageItem: MessageItem)
|
||||||
|
suspend fun addTimeStamp(timestampMessage: String)
|
||||||
|
suspend fun getLastEntry(): MessageEntity?
|
||||||
}
|
}
|
||||||
@@ -2,28 +2,54 @@ package com.example.h_mal.messengerapp.data.repository
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.example.h_mal.messengerapp.data.network.MessengerApi
|
import com.example.h_mal.messengerapp.data.network.MessengerApi
|
||||||
|
import com.example.h_mal.messengerapp.data.network.model.MessageItem
|
||||||
|
import com.example.h_mal.messengerapp.data.room.AppRoomDatabase
|
||||||
|
import com.example.h_mal.messengerapp.data.room.MessageEntity
|
||||||
import com.tinder.scarlet.WebSocket
|
import com.tinder.scarlet.WebSocket
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
import kotlinx.coroutines.channels.consumeEach
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class RepositoryImpl(
|
class RepositoryImpl(
|
||||||
private val messengerApi: MessengerApi
|
private val messengerApi: MessengerApi,
|
||||||
|
private val database: AppRoomDatabase
|
||||||
) : Repository{
|
) : Repository{
|
||||||
|
|
||||||
override fun observeText(): ReceiveChannel<String> {
|
override fun observeWebsocketMessage(): ReceiveChannel<MessageItem> {
|
||||||
return messengerApi.observerMessage()
|
return messengerApi.observerMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun submitMessage(message: String){
|
override fun observeWebsocketEvent(): ReceiveChannel<WebSocket.Event> {
|
||||||
val send = messengerApi.send(message)
|
return messengerApi.observerEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message to websocket or throw error if no connection
|
||||||
|
override fun submitMessageToApi(message: String, isSender: Boolean){
|
||||||
|
val messageItem = MessageItem(message, isSender)
|
||||||
|
val send = messengerApi.send(messageItem)
|
||||||
if (!send){
|
if (!send){
|
||||||
throw IOException("Could not send message")
|
throw IOException("Could not send message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEvent(): ReceiveChannel<WebSocket.Event.OnConnectionFailed> {
|
override fun getMessagesLiveData() = database.getMessageDao().getAllItems()
|
||||||
return messengerApi.observerFailure()
|
|
||||||
|
override suspend fun addMessageItemToDatabase(
|
||||||
|
messageItem: MessageItem
|
||||||
|
){
|
||||||
|
val entity = MessageEntity(messageItem)
|
||||||
|
database.getMessageDao().saveSingleItem(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addTimeStamp(
|
||||||
|
timestampMessage: String
|
||||||
|
){
|
||||||
|
val entity = MessageEntity(timestampMessage)
|
||||||
|
database.getMessageDao().saveSingleItem(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getLastEntry(): MessageEntity? {
|
||||||
|
return database.getMessageDao().getLastEntry()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -23,4 +23,7 @@ interface MessageDao {
|
|||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
fun deleteSingleEntry(message: MessageEntity)
|
fun deleteSingleEntry(message: MessageEntity)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM MessageEntity ORDER BY timestamp DESC LIMIT 1")
|
||||||
|
suspend fun getLastEntry(): MessageEntity?
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,32 @@ package com.example.h_mal.messengerapp.data.room
|
|||||||
|
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import com.example.h_mal.messengerapp.data.network.model.MessageItem
|
||||||
|
|
||||||
|
const val TWO_HOURS = 20 * 1000
|
||||||
@Entity
|
@Entity
|
||||||
data class MessageEntity(
|
data class MessageEntity(
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val sender: Boolean,
|
var isSender: Boolean? = null,
|
||||||
val message: String
|
val message: String?,
|
||||||
|
val timeStamp: Long = System.currentTimeMillis()
|
||||||
|
){
|
||||||
|
|
||||||
|
constructor(timeStampMessage: String): this(
|
||||||
|
0, null, timeStampMessage
|
||||||
)
|
)
|
||||||
|
|
||||||
|
constructor(messageItem: MessageItem): this(
|
||||||
|
0,
|
||||||
|
messageItem.isSender,
|
||||||
|
messageItem.message,
|
||||||
|
messageItem.timeStamp
|
||||||
|
)
|
||||||
|
|
||||||
|
fun isGreaterThanTwoHours(): Boolean{
|
||||||
|
val current = System.currentTimeMillis()
|
||||||
|
val time = current - timeStamp
|
||||||
|
return time > TWO_HOURS
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,16 +5,21 @@ import androidx.fragment.app.Fragment
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.example.h_mal.messengerapp.R
|
import com.example.h_mal.messengerapp.R
|
||||||
|
import com.example.h_mal.messengerapp.utils.showToast
|
||||||
import kotlinx.android.synthetic.main.main_fragment.*
|
import kotlinx.android.synthetic.main.main_fragment.*
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.InternalCoroutinesApi
|
|
||||||
import org.kodein.di.KodeinAware
|
import org.kodein.di.KodeinAware
|
||||||
import org.kodein.di.android.x.kodein
|
import org.kodein.di.android.x.kodein
|
||||||
import org.kodein.di.generic.instance
|
import org.kodein.di.generic.instance
|
||||||
|
|
||||||
class MainFragment : Fragment(), KodeinAware {
|
class MainFragment : Fragment(), KodeinAware {
|
||||||
|
// Kodein DI to retrieve @MainViewModelFactory
|
||||||
override val kodein by kodein()
|
override val kodein by kodein()
|
||||||
private val factory by instance<MainViewModelFactory>()
|
private val factory by instance<MainViewModelFactory>()
|
||||||
|
|
||||||
@@ -22,8 +27,6 @@ class MainFragment : Fragment(), KodeinAware {
|
|||||||
fun newInstance() = MainFragment()
|
fun newInstance() = MainFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@InternalCoroutinesApi
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
private lateinit var viewModel: MainViewModel
|
private lateinit var viewModel: MainViewModel
|
||||||
|
|
||||||
@@ -32,17 +35,53 @@ class MainFragment : Fragment(), KodeinAware {
|
|||||||
return inflater.inflate(R.layout.main_fragment, container, false)
|
return inflater.inflate(R.layout.main_fragment, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@InternalCoroutinesApi
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this, factory).get(MainViewModel::class.java)
|
viewModel = ViewModelProvider(this, factory).get(MainViewModel::class.java)
|
||||||
|
|
||||||
|
// Toast viewmodel error messages
|
||||||
|
viewModel.operationFailure.observe(viewLifecycleOwner, Observer {
|
||||||
|
it.getContentIfNotHandled()?.let { operationFailure ->
|
||||||
|
context?.showToast(operationFailure)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
message.setOnClickListener {
|
message_recycler.apply {
|
||||||
viewModel.submitMessage("dsfsdfsdjf")
|
layoutManager = LinearLayoutManager(requireContext()).apply {
|
||||||
|
stackFromEnd = true
|
||||||
|
}
|
||||||
|
setHasFixedSize(true)
|
||||||
|
|
||||||
|
// Apply adapter to recycler view
|
||||||
|
// anchor to the bottom when chats are being populated
|
||||||
|
adapter = MessageRecyclerAdapter(viewModel.messagesLiveData).apply {
|
||||||
|
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver(){
|
||||||
|
override fun onChanged() {
|
||||||
|
super.onChanged()
|
||||||
|
scrollToPosition(itemCount - 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
message_box_et.apply {
|
||||||
|
setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
when (actionId) {
|
||||||
|
EditorInfo.IME_ACTION_DONE ->{
|
||||||
|
viewModel.submitMessage(text.toString())
|
||||||
|
text.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
submit.setOnClickListener {
|
||||||
|
viewModel.submitMessage(text.toString())
|
||||||
|
text.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,95 @@
|
|||||||
package com.example.h_mal.messengerapp.ui.main
|
package com.example.h_mal.messengerapp.ui.main
|
||||||
|
|
||||||
import android.util.Log
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.example.h_mal.messengerapp.data.network.model.MessageItem
|
||||||
import com.example.h_mal.messengerapp.data.repository.Repository
|
import com.example.h_mal.messengerapp.data.repository.Repository
|
||||||
import com.tinder.scarlet.Stream
|
import com.example.h_mal.messengerapp.utils.Event
|
||||||
|
import com.example.h_mal.messengerapp.utils.convertDateEpoch
|
||||||
import com.tinder.scarlet.WebSocket
|
import com.tinder.scarlet.WebSocket
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.InternalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.channels.consumeEach
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
import kotlinx.coroutines.launch
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
|
||||||
@InternalCoroutinesApi
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
private val repository: Repository
|
private val repository: Repository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
// get live data from the database
|
||||||
|
val messagesLiveData = repository.getMessagesLiveData()
|
||||||
|
// Event live data to push error messages to the UI
|
||||||
|
val operationFailure = MutableLiveData<Event<String>>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.observeText().consumeEach {
|
repository.observeWebsocketMessage().consumeEach {
|
||||||
Log.i("WebsocketOut", it)
|
// add message to room database
|
||||||
|
addMessageItemToDatabase(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
repository.observeWebsocketEvent().consumeEach {
|
||||||
|
when(it){
|
||||||
|
is WebSocket.Event.OnConnectionOpened<*> ->{
|
||||||
|
saveDateStamp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Can handle different websocket events here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun submitMessage(message: String){
|
fun submitMessage(message: String){
|
||||||
repository.submitMessage(message)
|
if (message.isBlank()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Submit my message
|
||||||
|
repository.submitMessageToApi(message.trim(), true)
|
||||||
|
// Submit a random reply message with a delay of a second
|
||||||
|
Timer().schedule(1000){
|
||||||
|
repository.submitMessageToApi(randomString(), false)
|
||||||
|
}
|
||||||
|
}catch (exception: IOException){
|
||||||
|
val failureMessage = exception.message ?: "Failed to send"
|
||||||
|
operationFailure.postValue(Event(failureMessage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addMessageItemToDatabase(
|
||||||
|
messageItem: MessageItem
|
||||||
|
){
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
repository.addMessageItemToDatabase(messageItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveDateStamp(){
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
// get last entry from room database
|
||||||
|
val lastEntry = repository.getLastEntry() ?: return@launch
|
||||||
|
// if its greater than 2 hours
|
||||||
|
if (lastEntry.isGreaterThanTwoHours()){
|
||||||
|
lastEntry.timeStamp.convertDateEpoch()?.let {
|
||||||
|
repository.addTimeStamp(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a random string of 15 characters to simulate a reply
|
||||||
|
private fun randomString(): String {
|
||||||
|
val charPool = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
val randomString = (1..15)
|
||||||
|
.map { i -> Random.nextInt(0, charPool.length) }
|
||||||
|
.map(charPool::get)
|
||||||
|
.joinToString("");
|
||||||
|
|
||||||
|
return randomString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.example.h_mal.messengerapp.ui.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.h_mal.messengerapp.R
|
||||||
|
import com.example.h_mal.messengerapp.data.room.MessageEntity
|
||||||
|
import com.example.h_mal.messengerapp.utils.setColour
|
||||||
|
import kotlinx.android.synthetic.main.simple_message_layout.view.*
|
||||||
|
import kotlinx.android.synthetic.main.timestamp_item_layout.view.*
|
||||||
|
|
||||||
|
const val SENT_CONST = 101
|
||||||
|
const val RECEIVED_CONST = 102
|
||||||
|
const val TIMESTAMP_CONST = 103
|
||||||
|
class MessageRecyclerAdapter(
|
||||||
|
val messageLiveData: LiveData<List<MessageEntity>>
|
||||||
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
|
var messages: List<MessageEntity>? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Observer live data and update list on change
|
||||||
|
messageLiveData.observeForever{
|
||||||
|
messages = it
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): RecyclerView.ViewHolder {
|
||||||
|
return when(viewType){
|
||||||
|
SENT_CONST ->{
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.message_right, parent, false)
|
||||||
|
CustomViewHolder(view)
|
||||||
|
}
|
||||||
|
RECEIVED_CONST ->{
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.message_left, parent, false)
|
||||||
|
CustomViewHolder(view)
|
||||||
|
}
|
||||||
|
TIMESTAMP_CONST ->{
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.timestamp_item_layout, parent, false)
|
||||||
|
TimestampViewHolder(view)
|
||||||
|
}
|
||||||
|
else -> EmptyViewHolder(parent.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return messages?.size ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
when (holder){
|
||||||
|
is CustomViewHolder ->{
|
||||||
|
messages?.get(position)?.let {
|
||||||
|
holder.populateMessage(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TimestampViewHolder ->{
|
||||||
|
messages?.get(position)?.let {
|
||||||
|
holder.populateMessage(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
messages?.get(position)?.isSender?.let {
|
||||||
|
return when(it){
|
||||||
|
true -> SENT_CONST
|
||||||
|
false -> RECEIVED_CONST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TIMESTAMP_CONST
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val messageTv: TextView = itemView.inner_text
|
||||||
|
private val card: CardView = itemView.card
|
||||||
|
|
||||||
|
fun populateMessage(messageEntity: MessageEntity){
|
||||||
|
messageTv.text = messageEntity.message
|
||||||
|
if (messageEntity.isSender!!){
|
||||||
|
card.setColour(R.color.senderColour)
|
||||||
|
}else{
|
||||||
|
card.setColour(R.color.receiverColour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimestampViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val timestampTV: TextView = itemView.title
|
||||||
|
|
||||||
|
fun populateMessage(messageEntity: MessageEntity){
|
||||||
|
timestampTV.text = messageEntity.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmptyViewHolder(context: Context): RecyclerView.ViewHolder(View(context))
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.example.h_mal.messengerapp.utils
|
||||||
|
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
fun Long.convertDateEpoch(): String? {
|
||||||
|
return try {
|
||||||
|
val sdf = SimpleDateFormat("dd/MM/yy hh:mm a", Locale.getDefault())
|
||||||
|
val date = Date(this)
|
||||||
|
sdf.format(date)
|
||||||
|
}catch (e: Exception){
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.example.h_mal.messengerapp.utils
|
||||||
|
|
||||||
|
open class Event<out T>(private val content: T) {
|
||||||
|
|
||||||
|
var hasBeenHandled = false
|
||||||
|
private set // Allow external read but not write
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content and prevents its use again.
|
||||||
|
*/
|
||||||
|
fun getContentIfNotHandled(): T? {
|
||||||
|
return if (hasBeenHandled) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
hasBeenHandled = true
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content, even if it's already been handled.
|
||||||
|
*/
|
||||||
|
fun peekContent(): T = content
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.example.h_mal.messengerapp.utils
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import com.example.h_mal.messengerapp.R
|
||||||
|
|
||||||
|
fun View.show() {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.hide() {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.showToast(message: String) {
|
||||||
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.showToast(@StringRes resourceId: Int) {
|
||||||
|
Toast.makeText(this, resourceId, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.hideKeyboard() {
|
||||||
|
val inputMethodManager =
|
||||||
|
context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
inputMethodManager.hideSoftInputFromWindow(windowToken, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CardView.setColour(@ColorRes id: Int){
|
||||||
|
val colour = context.getColor(id)
|
||||||
|
setCardBackgroundColor(colour)
|
||||||
|
}
|
||||||
@@ -5,16 +5,50 @@
|
|||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingTop="@dimen/activity_vertical_margin"
|
||||||
|
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||||
|
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||||
tools:context=".ui.main.MainFragment">
|
tools:context=".ui.main.MainFragment">
|
||||||
|
|
||||||
<TextView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/message"
|
android:id="@+id/message_recycler"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginBottom="@dimen/activity_vertical_margin"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/message_typer_coptainer"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:listitem="@layout/message_left"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/message_typer_coptainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/message_box_et"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/activity_vertical_margin"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:maxLines="4"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="text"
|
||||||
|
tools:lines="4" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/submit"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="MainFragment"
|
android:layout_gravity="center|top"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:adjustViewBounds="true"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:src="@android:drawable/ic_menu_send" />
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
</LinearLayout>
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
18
app/src/main/res/layout/message_left.xml
Normal file
18
app/src/main/res/layout/message_left.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="6dp"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/simple_message_layout"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:elevation="0dp"
|
||||||
|
app:layout_constraintWidth_percent="0.8"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
16
app/src/main/res/layout/message_right.xml
Normal file
16
app/src/main/res/layout/message_right.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/simple_message_layout"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
app:layout_constraintWidth_percent="0.8"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
27
app/src/main/res/layout/simple_message_layout.xml
Normal file
27
app/src/main/res/layout/simple_message_layout.xml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardCornerRadius="12dp"
|
||||||
|
android:layout_margin="6dp"
|
||||||
|
tools:cardBackgroundColor="@color/senderColour">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/TextAppearance.AppCompat"
|
||||||
|
android:elevation="4dp"
|
||||||
|
android:id="@+id/inner_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="6dp"
|
||||||
|
tools:text="@string/dummy_long_text" />
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
23
app/src/main/res/layout/timestamp_item_layout.xml
Normal file
23
app/src/main/res/layout/timestamp_item_layout.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="?android:attr/textAppearancePopupMenuHeader"
|
||||||
|
android:gravity="center"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:fadingEdge="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?android:attr/listPreferredItemHeightSmall"
|
||||||
|
tools:text="Title"/>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -3,4 +3,7 @@
|
|||||||
<color name="colorPrimary">#6200EE</color>
|
<color name="colorPrimary">#6200EE</color>
|
||||||
<color name="colorPrimaryDark">#3700B3</color>
|
<color name="colorPrimaryDark">#3700B3</color>
|
||||||
<color name="colorAccent">#03DAC5</color>
|
<color name="colorAccent">#03DAC5</color>
|
||||||
|
|
||||||
|
<color name="senderColour">#BB8AFF</color>
|
||||||
|
<color name="receiverColour">#EBE2F6</color>
|
||||||
</resources>
|
</resources>
|
||||||
5
app/src/main/res/values/dimens.xml
Normal file
5
app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<resources>
|
||||||
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
|
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||||
|
</resources>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">MessengerApp</string>
|
<string name="app_name">MessengerApp</string>
|
||||||
|
<string name="dummy_long_text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user