diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 8a0a00a..17ca97e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ android { defaultConfig { applicationId "com.example.h_mal.messengerapp" - minSdkVersion 23 + minSdkVersion 24 targetSdkVersion 30 versionCode 1 versionName "1.0" @@ -42,6 +42,8 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'androidx.lifecycle:lifecycle-extensions: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' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' diff --git a/app/src/main/java/com/example/h_mal/messengerapp/application/AppClass.kt b/app/src/main/java/com/example/h_mal/messengerapp/application/AppClass.kt index 7f1adf7..86d9602 100644 --- a/app/src/main/java/com/example/h_mal/messengerapp/application/AppClass.kt +++ b/app/src/main/java/com/example/h_mal/messengerapp/application/AppClass.kt @@ -39,7 +39,7 @@ class AppClass: Application(), KodeinAware{ ) } bind() from singleton { AppRoomDatabase(instance()) } - bind() from singleton { RepositoryImpl(instance()) } + bind() from singleton { RepositoryImpl(instance(), instance()) } bind() from provider { MainViewModelFactory(instance()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/messengerapp/data/network/MessengerApi.kt b/app/src/main/java/com/example/h_mal/messengerapp/data/network/MessengerApi.kt index a49749b..5b6537a 100644 --- a/app/src/main/java/com/example/h_mal/messengerapp/data/network/MessengerApi.kt +++ b/app/src/main/java/com/example/h_mal/messengerapp/data/network/MessengerApi.kt @@ -1,5 +1,6 @@ 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.Stream import com.tinder.scarlet.WebSocket @@ -14,14 +15,16 @@ import okhttp3.OkHttpClient interface MessengerApi { + // Receive websocket messages in the form of string @Receive - fun observerMessage(): ReceiveChannel + fun observerMessage(): ReceiveChannel @Receive - fun observerFailure(): ReceiveChannel + fun observerEvent(): ReceiveChannel + // Send message to websocket and return pass/fail boolean result @Send - fun send(message: Any): Boolean + fun send(message: MessageItem): Boolean // invoke method creating an invocation of the api call @@ -32,7 +35,7 @@ interface MessengerApi { val webSocketUrl = "ws://echo.websocket.org/" - // creation of retrofit class + // creation of Api class for websocket return Scarlet.Builder() .webSocketFactory(okkHttpClient.newWebSocketFactory(webSocketUrl)) .addMessageAdapterFactory(GsonMessageAdapter.Factory()) diff --git a/app/src/main/java/com/example/h_mal/messengerapp/data/network/model/MessageItem.kt b/app/src/main/java/com/example/h_mal/messengerapp/data/network/model/MessageItem.kt new file mode 100644 index 0000000..7833424 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/messengerapp/data/network/model/MessageItem.kt @@ -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() +) \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/messengerapp/data/repository/Repository.kt b/app/src/main/java/com/example/h_mal/messengerapp/data/repository/Repository.kt index 8ac1ef7..e65b222 100644 --- a/app/src/main/java/com/example/h_mal/messengerapp/data/repository/Repository.kt +++ b/app/src/main/java/com/example/h_mal/messengerapp/data/repository/Repository.kt @@ -1,11 +1,19 @@ 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 kotlinx.coroutines.channels.ReceiveChannel interface Repository { - fun observeText(): ReceiveChannel - fun submitMessage(message: String) - fun getEvent(): ReceiveChannel + + fun observeWebsocketMessage(): ReceiveChannel + fun observeWebsocketEvent(): ReceiveChannel + fun submitMessageToApi(message: String, isSender: Boolean) + fun getMessagesLiveData(): LiveData> + suspend fun addMessageItemToDatabase(messageItem: MessageItem) + suspend fun addTimeStamp(timestampMessage: String) + suspend fun getLastEntry(): MessageEntity? } \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/messengerapp/data/repository/RepositoryImpl.kt b/app/src/main/java/com/example/h_mal/messengerapp/data/repository/RepositoryImpl.kt index f91cabc..401fcda 100644 --- a/app/src/main/java/com/example/h_mal/messengerapp/data/repository/RepositoryImpl.kt +++ b/app/src/main/java/com/example/h_mal/messengerapp/data/repository/RepositoryImpl.kt @@ -2,28 +2,54 @@ package com.example.h_mal.messengerapp.data.repository import android.util.Log 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 kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.consumeEach import java.io.IOException class RepositoryImpl( - private val messengerApi: MessengerApi + private val messengerApi: MessengerApi, + private val database: AppRoomDatabase ) : Repository{ - override fun observeText(): ReceiveChannel { + override fun observeWebsocketMessage(): ReceiveChannel { return messengerApi.observerMessage() } - override fun submitMessage(message: String){ - val send = messengerApi.send(message) + override fun observeWebsocketEvent(): ReceiveChannel { + 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){ throw IOException("Could not send message") } } - override fun getEvent(): ReceiveChannel { - return messengerApi.observerFailure() + override fun getMessagesLiveData() = database.getMessageDao().getAllItems() + + 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() } } \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/messengerapp/data/room/MessageDao.kt b/app/src/main/java/com/example/h_mal/messengerapp/data/room/MessageDao.kt index e1d1aab..4db5b7b 100644 --- a/app/src/main/java/com/example/h_mal/messengerapp/data/room/MessageDao.kt +++ b/app/src/main/java/com/example/h_mal/messengerapp/data/room/MessageDao.kt @@ -23,4 +23,7 @@ interface MessageDao { @Delete fun deleteSingleEntry(message: MessageEntity) + + @Query("SELECT * FROM MessageEntity ORDER BY timestamp DESC LIMIT 1") + suspend fun getLastEntry(): MessageEntity? } \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/messengerapp/data/room/MessageEntity.kt b/app/src/main/java/com/example/h_mal/messengerapp/data/room/MessageEntity.kt index 986d6e5..a406f0a 100644 --- a/app/src/main/java/com/example/h_mal/messengerapp/data/room/MessageEntity.kt +++ b/app/src/main/java/com/example/h_mal/messengerapp/data/room/MessageEntity.kt @@ -2,11 +2,32 @@ package com.example.h_mal.messengerapp.data.room import androidx.room.Entity import androidx.room.PrimaryKey +import com.example.h_mal.messengerapp.data.network.model.MessageItem +const val TWO_HOURS = 20 * 1000 @Entity data class MessageEntity( @PrimaryKey(autoGenerate = true) val id: Int, - val sender: Boolean, - val message: String -) \ No newline at end of file + var isSender: Boolean? = null, + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/messengerapp/ui/main/MainFragment.kt b/app/src/main/java/com/example/h_mal/messengerapp/ui/main/MainFragment.kt index 2e92761..b643cfc 100644 --- a/app/src/main/java/com/example/h_mal/messengerapp/ui/main/MainFragment.kt +++ b/app/src/main/java/com/example/h_mal/messengerapp/ui/main/MainFragment.kt @@ -5,16 +5,21 @@ import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import androidx.lifecycle.Observer 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.utils.showToast import kotlinx.android.synthetic.main.main_fragment.* import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.InternalCoroutinesApi import org.kodein.di.KodeinAware import org.kodein.di.android.x.kodein import org.kodein.di.generic.instance class MainFragment : Fragment(), KodeinAware { + // Kodein DI to retrieve @MainViewModelFactory override val kodein by kodein() private val factory by instance() @@ -22,8 +27,6 @@ class MainFragment : Fragment(), KodeinAware { fun newInstance() = MainFragment() } - - @InternalCoroutinesApi @ExperimentalCoroutinesApi private lateinit var viewModel: MainViewModel @@ -32,17 +35,53 @@ class MainFragment : Fragment(), KodeinAware { return inflater.inflate(R.layout.main_fragment, container, false) } - @InternalCoroutinesApi @ExperimentalCoroutinesApi - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) 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 { - viewModel.submitMessage("dsfsdfsdjf") + message_recycler.apply { + 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() + } } } diff --git a/app/src/main/java/com/example/h_mal/messengerapp/ui/main/MainViewModel.kt b/app/src/main/java/com/example/h_mal/messengerapp/ui/main/MainViewModel.kt index eb8dd61..64c97d0 100644 --- a/app/src/main/java/com/example/h_mal/messengerapp/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/example/h_mal/messengerapp/ui/main/MainViewModel.kt @@ -1,32 +1,95 @@ package com.example.h_mal.messengerapp.ui.main -import android.util.Log +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel 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.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 kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.* 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 class MainViewModel( private val repository: Repository ) : ViewModel() { + // get live data from the database + val messagesLiveData = repository.getMessagesLiveData() + // Event live data to push error messages to the UI + val operationFailure = MutableLiveData>() + init { viewModelScope.launch { - repository.observeText().consumeEach { - Log.i("WebsocketOut", it) + repository.observeWebsocketMessage().consumeEach { + // 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){ - 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 } } \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/messengerapp/ui/main/MessageRecyclerAdapter.kt b/app/src/main/java/com/example/h_mal/messengerapp/ui/main/MessageRecyclerAdapter.kt new file mode 100644 index 0000000..ece4e35 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/messengerapp/ui/main/MessageRecyclerAdapter.kt @@ -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> +) : RecyclerView.Adapter() { + + var messages: List? = 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)) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/messengerapp/utils/DateUtils.kt b/app/src/main/java/com/example/h_mal/messengerapp/utils/DateUtils.kt new file mode 100644 index 0000000..faaac0d --- /dev/null +++ b/app/src/main/java/com/example/h_mal/messengerapp/utils/DateUtils.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/messengerapp/utils/Event.kt b/app/src/main/java/com/example/h_mal/messengerapp/utils/Event.kt new file mode 100644 index 0000000..4204004 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/messengerapp/utils/Event.kt @@ -0,0 +1,24 @@ +package com.example.h_mal.messengerapp.utils + +open class Event(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 +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/messengerapp/utils/ViewUtils.kt b/app/src/main/java/com/example/h_mal/messengerapp/utils/ViewUtils.kt new file mode 100644 index 0000000..7faf11f --- /dev/null +++ b/app/src/main/java/com/example/h_mal/messengerapp/utils/ViewUtils.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/res/layout/main_fragment.xml b/app/src/main/res/layout/main_fragment.xml index 1196542..b5246e4 100644 --- a/app/src/main/res/layout/main_fragment.xml +++ b/app/src/main/res/layout/main_fragment.xml @@ -5,16 +5,50 @@ android:id="@+id/main" android:layout_width="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"> - + + + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/message_left.xml b/app/src/main/res/layout/message_left.xml new file mode 100644 index 0000000..668e6f8 --- /dev/null +++ b/app/src/main/res/layout/message_left.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/message_right.xml b/app/src/main/res/layout/message_right.xml new file mode 100644 index 0000000..5d66a77 --- /dev/null +++ b/app/src/main/res/layout/message_right.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/simple_message_layout.xml b/app/src/main/res/layout/simple_message_layout.xml new file mode 100644 index 0000000..7806187 --- /dev/null +++ b/app/src/main/res/layout/simple_message_layout.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/timestamp_item_layout.xml b/app/src/main/res/layout/timestamp_item_layout.xml new file mode 100644 index 0000000..6bfebb6 --- /dev/null +++ b/app/src/main/res/layout/timestamp_item_layout.xml @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 4faecfa..4405807 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,4 +3,7 @@ #6200EE #3700B3 #03DAC5 + + #BB8AFF + #EBE2F6 \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..47c8224 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68d1041..6e7e7f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ MessengerApp + 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. \ No newline at end of file