diff --git a/app/build.gradle b/app/build.gradle index 54e4eac..6e4f0ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,13 +4,17 @@ plugins { } android { - compileSdk 34 + compileSdk 35 namespace "otus.gpb.recyclerview" + buildFeatures { + viewBinding true + } + defaultConfig { applicationId "otus.gpb.recyclerview" minSdk 26 - targetSdk 34 + targetSdk 35 versionCode 1 versionName "1.0" @@ -34,11 +38,14 @@ android { dependencies { - implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.5.1' - implementation 'com.google.android.material:material:1.7.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.core:core-ktx:1.16.0' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' + implementation 'com.github.bumptech.glide:glide:4.16.0' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + implementation('androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0') + implementation('androidx.lifecycle:lifecycle-runtime-ktx:2.9.0') } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ef75335..9ed9ff2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ - + create(modelClass: Class): T { + return ChatViewModel(repository) as T + } + })[ChatViewModel::class.java] + + lifecycleScope.launch { + viewModel.chats.collect { chats -> + adapter.submitList(chats) + } + } + + recyclerView.addItemDecoration(ChatItemDecorator(this)) + recyclerView.apply { + clipChildren = false + clipToPadding = false + } + + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val lastVisibleChat = layoutManager.findLastVisibleItemPosition() + val chatsCount = layoutManager.itemCount + + if (!viewModel.isLoading.value && lastVisibleChat >= chatsCount - 5) { + viewModel.loadMoreChats() + } + } + }) + + recyclerView.adapter = adapter + ItemTouchHelper(ChatItemTouchHelper(viewModel)).attachToRecyclerView(recyclerView) + } } \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/data/Chat.kt b/app/src/main/java/otus/gpb/recyclerview/data/Chat.kt new file mode 100644 index 0000000..76a937a --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/data/Chat.kt @@ -0,0 +1,69 @@ +package otus.gpb.recyclerview.data + +import java.time.Duration +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Locale + +enum class MessageStatus { + READ, DELIVERED +} + +enum class MessageDirection { + IN, OUT +} + +data class Chat( + var id: Int, + val user: User, // user with whom the chat is held + val timeRead: Instant? = null, // Time when potentially the chat was seen/"read" status + val isScam: Boolean = false, // Automatically (or manually) marked as spam + val mutedUntil: Instant? = null, // Time until chat is muted, i.e. you won't get notifications from the chat; + val isMutedForever: Boolean = false, + var isArchived: Boolean, // The chat is archived, it won't be displayed in the main chat list + var isVoip: Boolean = false, // Has voice message (?) + var timeLast: Instant? = null, + var textLast: String = "", + var isMentionedLast: Boolean = false, + var statusLast: MessageStatus? = null, + var unreadCount: Int = 0, + var directionLast: MessageDirection? = null // for displaying checkers in front of the last message date + +) { + fun isMuted(): Boolean { + return isMutedForever || (mutedUntil != null && mutedUntil.isAfter(Instant.now())) + } + + fun getLastMessageTime(): String { + val result: String = + if (timeLast == null) + "" + else { + val diff = Duration.between(timeLast, Instant.now()).abs() + val dateTimeWithTimeZone = timeLast!!.atZone(ZoneId.systemDefault()) + DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM) + .withLocale(Locale.getDefault()) + when { + diff.toHours() < 24 -> { + DateTimeFormatter.ofPattern("HH:mm").format(dateTimeWithTimeZone) + } + + diff.toDays() < 7 -> { + DateTimeFormatter.ofPattern("EEE").format(dateTimeWithTimeZone) + } + + diff.toDays() < 365 -> { + DateTimeFormatter.ofPattern("MMM dd").format(dateTimeWithTimeZone) + } + + else -> { + DateTimeFormatter.ofPattern("dd.MM.yyyy").format(dateTimeWithTimeZone) + } + } + } + return result + } + +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/data/ChatRepository.kt b/app/src/main/java/otus/gpb/recyclerview/data/ChatRepository.kt new file mode 100644 index 0000000..b718d1a --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/data/ChatRepository.kt @@ -0,0 +1,40 @@ +package otus.gpb.recyclerview.data + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update + +class ChatRepository { + private val _allChats = MutableStateFlow(emptyList()) + + private val _page = MutableStateFlow(0) + + fun getChatsFilteredAndSorted(): StateFlow> { + return _allChats.map { chats -> + chats.asSequence() + .filter { !it.isArchived } + .toList() + }.stateIn(CoroutineScope(Dispatchers.Default), SharingStarted.Eagerly, emptyList()) + } + + fun toggleArchiveChat(chatId: Int) { + _allChats.update { chats -> + chats.map { chat -> + if (chat.id == chatId) chat.copy(isArchived = !chat.isArchived) else chat + } + } + } + + fun loadMoreChats() { + if (_allChats.value.size < getChatsCount()) { + _page.update { it + 1 } + _allChats.update { chats -> chats + loadMoreChatsFromDB(_page.value) } + } + } +} + diff --git a/app/src/main/java/otus/gpb/recyclerview/data/ChatStorage.kt b/app/src/main/java/otus/gpb/recyclerview/data/ChatStorage.kt new file mode 100644 index 0000000..36c7cae --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/data/ChatStorage.kt @@ -0,0 +1,195 @@ +package otus.gpb.recyclerview.data + +import java.time.Instant +import java.time.temporal.ChronoUnit + +private val users = mutableSetOf() // all users on a platform + +val friendList = mutableSetOf() // added as friends + +private val chatDB: MutableList = emptyList().toMutableList() + +/** + * acts as database: + * - generates all chats, sorts by time (init mock database) + * - in main activity loads first page (slice of first pageSize records + * - when scrolling, loads next page. + */ + +private const val userCount = 100 +private const val maxFriends = 50 +private const val pageSize = 15 +private val lag = (50..100).random()..(100..200).random() + +private val userNames = setOf( + "Meadow Burton", + "Zander Rice", + "Ada Marin", + "Aldo Rogers", + "Madelyn Wiggins", + "Azariah Walters", + "Samara Mosley", + "Rayden Koch", + "Milana James", + "Jaxson Miller", + "Isabella Duffy", + "Kyng Frazier", + "Octavia Nicholson", + "Rodrigo Donaldson", + "Natasha Morrow", + "Kyree Taylor", + "Sofia Marquez", + "Malakai Mullen", + "Shay Byrd", + "Cristian Poole", + "Bonnie Carrillo", + "Wade Marin", + "Waldo Dorsey", + "Alejandro Miranda", + "Kieth Rosales", + "Diane Frazier", + "Angelita Finley", + "Brittany Becker", + "Anastasia Lyons", + "Johanna Jarvis", + "Tara Delacruz", + "Darron Jacobson", + "Margarito Dennis", + "Sheldon Sosa", + "Nelda Hawkins", + "Robt Fields", + "Cornelia Scott", + "Estella Mays", + "George Gray", + "Stanford Brooks", + "Lacy Meyers", + "Nicolas Shaffer", + "Beau Goodwin", + "Ira Mcgrath" +) + +private val randomStatuses = setOf( + "Twenty one year old guys are like car thiefs, they tend to get the easy ones.", + "Why do people with bad breath always have to tell me a damn secrets???", + "LIKE IF: Knowing you`re totally screwed for an exam, but staying on facebook and not studying.", + "I need to be blessed with the abilty to start studying..... because studying is not a part of my life.... #IHateFinals!!!", + "the awkward moment when you forget how to fly.....", + "The awkward moment when you need to send that unfashionable friend to Mallzee.", + "Who else used to think Courage the Cowardly dog was the scariest show ever?", + "They keep saying the right person will come along; I think a truck hit mine.", + "Everyone judges. So do whatever you want, the ones who love you will never leave.", + "A girl`s smile hides thousand words, a girl`s tears hides thousand feelings.", + "Getting a text from that special someone right when you`ve felt like they forgot about you.", + "This looks perfect", + "I swear, school wouldn`t be half as bad if we didn`t have to wake up so damn early.", + "The only people who truly know your story are the ones that helped you write it.", + "Age does not protect you from love, but love to some extent protects you from age.", + "When I hear myself eating crunchy food, I wonder if other people can hear it too.", + "Like if: Saying random numbers when someone is counting to make them lose count", + "One of these days common sense will come back in fashion.", + "Some say long distance relationships never succeed, I say with enough effort, time and commitment, love will find its way.", + "I’m constantly torn between the ‘be kind to everyone’ and the ‘fuck everyone you owe them nothing’ mentalities.", + "LIKE IF: I look at my watch, and when someone asks the time, I have to look again.", + "Letters begin with ABC. Numbers begin with 123. Music begins with Do Re Mi. Love begins with you and me.", + "Ever since I met you, it hasn`t been the same. All you got me doing is drawing hearts around your name.", + "Like if you ever fell down the stairs thinking you were walking down a slope.", + "Cellphone, laptop, iPod, TV = My best friends.", + "Facebook is like a refrigerator. You get bored and keep checking, but nothing ever changes.", + "Dear eyelashes, wishbones, pennies, shooting stars, 11:11 and birthday candles. What happened to my wishes? Sincerely, still waiting.", + "WHEN YOU TYPE LIKE THIS IT MAKES ME THINK YOU`RE YELLING, when u type like dis it makes me think u a lil kidd, WhEn YoU TYpE LIkE ThIs It MAkEs ME ThInk YOUr MEntAllY HAndY CAppEd", + "Dear everyone, always remember that when you fall, I`ll pick you up. But just AFTER I finish laughing.", + "do we have time for me? no no no no no no no no no no no no", + "I always pretend to care about teachers personal life, to waste time in their class.", + "LIKE IF: Telling your mom something funny & ending up getting yelled at.", + "I have to plug my mobile phone into the charger so much throughout the day, that I basically have a landline again.", + "Having problems are inevitable, but being stressed out because of those problems is an option.", + "If Monday were shoes, they`d be Crocs.", + "Accidentally erase something you just typed on your iPhone? To undo that, just shake it!", + "I`m sorta, kinda, maybe, slightly, just a little tiny bit, addicted to you.", + "Oooooh, thats a bit too harsh. Let me put a `lol` at the end of it.", + "Dear Santa, this year, please send clothes for all those poor ladies in Daddy`s computer.", + "they say best friends are hard to find: well that`s cause the best ones are already mine...:)", +) + + +private fun generateUsers() { + for (i in 1..userCount) { + users.add( + User( + id = if (users.isEmpty()) 1 else users.maxOf { it.id } + 1, + name = (userNames).random(), + avatar = "https://i.pravatar.cc/150?img=${(1..70).random()}", + lastSeen = getRandomTime(), + isVerified = (0..10).random() < 2, + statusMessage = randomStatuses.randomOrNull(), + nickName = "" + ) + ) + } +} + +private fun befriendRandomUsers() { + for (i in 1..maxFriends) { + friendList.add((users - friendList).random()) + } +} + +private fun generateChats() { + repeat(userCount) { + val chat = generateSingleChat() + chatDB.add(chat) + } + chatDB +} + +private fun generateSingleChat(): Chat { + // can be from the friend list as well as random unknown contacts + val fromFriendList = (0..10).random() < 7 + val chat = Chat( + user = if (fromFriendList) friendList.random() else (users - friendList).random(), + timeRead = getRandomTime(), + isScam = if (fromFriendList) false else ((0..3).random() < 1), + mutedUntil = null, + isMutedForever = (0..10).random() < 4, + isArchived = false, + isVoip = (0..10).random() < 2, + id = chatDB.maxOfOrNull { it.id }?.plus(1) ?: 1, + directionLast = if ((0..3).random() < 2) MessageDirection.IN else MessageDirection.OUT, + statusLast = if ((0..3).random() < 2) MessageStatus.READ else MessageStatus.DELIVERED, + textLast = randomStatuses.random(), + isMentionedLast = (0..10).random() < 2, + unreadCount = if ((0..3).random() < 3) 0 else (0..10).random(), + timeLast = getRandomTime(start = Instant.now().minus(666, ChronoUnit.DAYS)) + ) + chat.unreadCount = (0..10).random() + return chat +} + +fun initDB() { + generateUsers() + befriendRandomUsers() + generateChats() + chatDB.sortByDescending { it.timeLast } +} + +private fun getRandomTime( + start: Instant = Instant.now().minus(365, ChronoUnit.DAYS), + end: Instant = Instant.now() +): Instant { + val startSeconds = start.epochSecond + val endSeconds = end.epochSecond + val randomSeconds = (startSeconds until endSeconds).random() + return Instant.ofEpochSecond(randomSeconds) +} + +fun loadMoreChatsFromDB(page: Int): List { + return if ((page + 1) * pageSize > userCount) emptyList() + else { + Thread.sleep((lag).random().toLong()) // random delay as if getting data over the Internet + chatDB.slice(page * pageSize until (page + 1) * pageSize) + } +} + +fun getChatsCount(): Int { + return chatDB.size +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/data/ChatViewModel.kt b/app/src/main/java/otus/gpb/recyclerview/data/ChatViewModel.kt new file mode 100644 index 0000000..b3896db --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/data/ChatViewModel.kt @@ -0,0 +1,36 @@ +package otus.gpb.recyclerview.data + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class ChatViewModel(private val repository: ChatRepository) : ViewModel() { + val chats = repository.getChatsFilteredAndSorted() + + private val _isLoading = MutableStateFlow(false) + val isLoading = _isLoading.asStateFlow() + + init { + _isLoading.value = true + viewModelScope.launch { + repository.loadMoreChats() + _isLoading.value = false + } + } + + fun toggleArchiveChat(chatId: Int) { + repository.toggleArchiveChat(chatId) + } + + fun loadMoreChats() { + if (_isLoading.value) return + + _isLoading.value = true + viewModelScope.launch { + repository.loadMoreChats() + _isLoading.value = false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/data/User.kt b/app/src/main/java/otus/gpb/recyclerview/data/User.kt new file mode 100644 index 0000000..1375a22 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/data/User.kt @@ -0,0 +1,13 @@ +package otus.gpb.recyclerview.data + +import java.time.Instant + +data class User( + val id: Int, + val name: String, + val nickName: String = "", // I have no clue what this line actually is + val avatar: String, // Or Int? Some sort of a reference to an image + val lastSeen: Instant, + val isVerified: Boolean = false, + val statusMessage: String? = null // text of the status that was set by the user +) \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ui/ChatItemDecorator.kt b/app/src/main/java/otus/gpb/recyclerview/ui/ChatItemDecorator.kt new file mode 100644 index 0000000..d036cb0 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ui/ChatItemDecorator.kt @@ -0,0 +1,45 @@ +package otus.gpb.recyclerview.ui + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import otus.gpb.recyclerview.R + +class ChatItemDecorator(private val context: Context) : RecyclerView.ItemDecoration() { + + private val bounds = Rect() + private val paint = Paint().apply { + color = ContextCompat.getColor(context, R.color.color_less_attention) + strokeWidth = 0.5f + } + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDraw(c, parent, state) + + val childCount = parent.childCount + for (i in 0 until childCount) { + val child = parent.getChildAt(i) + parent.getDecoratedBoundsWithMargins(child, bounds) + + val positionCurrent = parent.getChildAdapterPosition(child) + if (positionCurrent != RecyclerView.NO_POSITION) { + val lastElementPosition = parent.adapter?.itemCount?.minus(1) + if (positionCurrent != lastElementPosition) { + c.drawLine( + (child.findViewById(R.id.img_user_avatar_container).right + + child.findViewById(R.id.txt_user_name).left.toFloat()) / 2, + bounds.bottom.toFloat(), + bounds.right.toFloat(), + bounds.bottom.toFloat(), + paint, + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ui/ChatItemTouchHelper.kt b/app/src/main/java/otus/gpb/recyclerview/ui/ChatItemTouchHelper.kt new file mode 100644 index 0000000..3ab3099 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ui/ChatItemTouchHelper.kt @@ -0,0 +1,50 @@ +package otus.gpb.recyclerview.ui + +import android.graphics.Canvas +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import otus.gpb.recyclerview.data.ChatViewModel + +class ChatItemTouchHelper(private val viewModel: ChatViewModel) : ItemTouchHelper.Callback() { + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + return makeMovementFlags(0, ItemTouchHelper.LEFT) + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + return false + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val position = viewHolder.adapterPosition + if (position != RecyclerView.NO_POSITION) { + val id = (viewHolder as ChatViewHolder).id + viewModel.toggleArchiveChat(id) + } + } + + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + (viewHolder as ChatViewHolder).updateSwipeState(dX) + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + (viewHolder as ChatViewHolder).resetSwipeState() + super.clearView(recyclerView, viewHolder) + } + +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ui/ChatListAdapter.kt b/app/src/main/java/otus/gpb/recyclerview/ui/ChatListAdapter.kt new file mode 100644 index 0000000..6bf6231 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ui/ChatListAdapter.kt @@ -0,0 +1,37 @@ +package otus.gpb.recyclerview.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import otus.gpb.recyclerview.data.Chat +import otus.gpb.recyclerview.databinding.VhChatsBinding + +class ChatListAdapter : ListAdapter(ChatDiffUtils()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val binding = VhChatsBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ChatViewHolder(binding) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + (holder as ChatViewHolder).resetSwipeState() + holder.bind(getItem(position)) + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + (holder as ChatViewHolder).resetSwipeState() + super.onViewRecycled(holder) + } +} + +private class ChatDiffUtils : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Chat, newItem: Chat): Boolean { + return (oldItem::class == newItem::class && oldItem.id == newItem.id) + } + + override fun areContentsTheSame(oldItem: Chat, newItem: Chat): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/java/otus/gpb/recyclerview/ui/ChatViewHolder.kt b/app/src/main/java/otus/gpb/recyclerview/ui/ChatViewHolder.kt new file mode 100644 index 0000000..d581608 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ui/ChatViewHolder.kt @@ -0,0 +1,106 @@ +package otus.gpb.recyclerview.ui + +import android.graphics.Color +import android.view.View +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import otus.gpb.recyclerview.R +import otus.gpb.recyclerview.data.Chat +import otus.gpb.recyclerview.data.MessageDirection +import otus.gpb.recyclerview.data.MessageStatus +import otus.gpb.recyclerview.databinding.VhChatsBinding +import kotlin.math.abs + +class ChatViewHolder(private val binding: VhChatsBinding) : + RecyclerView.ViewHolder(binding.root) { + + var id: Int = -1 + + private fun Boolean.setViewVisibility(): Int { + return if (this) View.VISIBLE else View.GONE + } + + private fun Chat.bindUserInfo(binding: VhChatsBinding) { + with(binding) { + txtUserName.text = user.name + if (user.nickName == "") + txtUserName2.visibility = View.GONE + else { + txtUserName2.visibility = View.VISIBLE + txtUserName2.text = user.nickName + } + Glide.with(itemView.context).load(user.avatar).centerCrop() + .into(imgUserAvatar) + imgUserVerified.visibility = user.isVerified.setViewVisibility() + } + } + + private fun Chat.bindMessageInfo(binding: VhChatsBinding) { + with(binding) { + txtLastMessageTime.text = getLastMessageTime() + txtLastMessage.text = textLast + + imgIsRead.visibility = + (statusLast == MessageStatus.READ && directionLast == MessageDirection.OUT).setViewVisibility() + imgIsSent.visibility = (statusLast == MessageStatus.DELIVERED && directionLast == MessageDirection.OUT).setViewVisibility() + + imgIsMuted.visibility = isMuted().setViewVisibility() + if (isMentionedLast || unreadCount == 0) { + imgBadgeRightContainer.visibility = View.GONE + } else { + imgBadgeRightContainer.visibility = View.VISIBLE + txtBadgeRight.text = + if (isMentionedLast) "@" else unreadCount.toString() + } + imgUserAvatarVoipContainer.visibility = when (isVoip) { + true -> View.VISIBLE + else -> View.GONE + } + imgLastMessageAttachment.visibility = View.GONE + imgIsScam.visibility = if (isScam) View.VISIBLE else View.GONE + } + } + + fun bind(chat: Chat) { + with(binding) { + this@ChatViewHolder.id = chat.id + chat.bindUserInfo(binding) + chat.bindMessageInfo(binding) + + swipeRevealContainer.visibility = View.INVISIBLE + container.translationX = 0f + } + } + + fun updateSwipeState(dX: Float) { + binding.container.translationX = dX + + val swipeThreshold = binding.swipeRevealContainer.width / 4 + + when { + (abs(dX) > swipeThreshold) -> + binding.swipeRevealContainer.setBackgroundColor(Color.GRAY) + + else -> + binding.swipeRevealContainer.setBackgroundColor( + ContextCompat.getColor( + binding.root.context, + R.color.color_accent_main + ) + ) + } + + binding.swipeRevealContainer.translationX = -dX + binding.swipeRevealContainer.visibility = when { + abs(dX) > 5f -> View.VISIBLE + else -> View.INVISIBLE + } + } + + fun resetSwipeState() { + binding.container.translationX = 0f + binding.guidelineRevealContainer.visibility = View.INVISIBLE + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/archive.xml b/app/src/main/res/drawable/archive.xml new file mode 100644 index 0000000..a2f1f70 --- /dev/null +++ b/app/src/main/res/drawable/archive.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/check_icon.xml b/app/src/main/res/drawable/check_icon.xml new file mode 100644 index 0000000..5b04999 --- /dev/null +++ b/app/src/main/res/drawable/check_icon.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/mute_icon.xml b/app/src/main/res/drawable/mute_icon.xml new file mode 100644 index 0000000..b8d745d --- /dev/null +++ b/app/src/main/res/drawable/mute_icon.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/preview_18dp.xml b/app/src/main/res/drawable/preview_18dp.xml new file mode 100644 index 0000000..57053ad --- /dev/null +++ b/app/src/main/res/drawable/preview_18dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/read_icon.xml b/app/src/main/res/drawable/read_icon.xml new file mode 100644 index 0000000..162b399 --- /dev/null +++ b/app/src/main/res/drawable/read_icon.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/verified_icon.xml b/app/src/main/res/drawable/verified_icon.xml new file mode 100644 index 0000000..761dac0 --- /dev/null +++ b/app/src/main/res/drawable/verified_icon.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/voip.xml b/app/src/main/res/drawable/voip.xml new file mode 100644 index 0000000..c2b0bbd --- /dev/null +++ b/app/src/main/res/drawable/voip.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/font/roboto_regular.ttf b/app/src/main/res/font/roboto_regular.ttf new file mode 100644 index 0000000..7e3bb2f Binary files /dev/null and b/app/src/main/res/font/roboto_regular.ttf differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2d026df..f1e694a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -3,11 +3,14 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".MainActivity"> + android:layout_height="match_parent" + tools:itemCount="4" + tools:listitem="@layout/vh_chats" /> \ No newline at end of file diff --git a/app/src/main/res/layout/vh_chats.xml b/app/src/main/res/layout/vh_chats.xml new file mode 100644 index 0000000..0c84ec8 --- /dev/null +++ b/app/src/main/res/layout/vh_chats.xml @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 f8c6127..2366f0b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,7 @@ #FF018786 #FF000000 #FFFFFFFF + #8D8E90 + #3D95D4 + #E64646 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d78b1f..3a788ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,14 @@ RecyclerView + Archive + User\'s avatar + This is VOIP + User name placeholder that can be quite long + User is verified + Chat is currently muted + \@ + May 02 + Last message received that can also can be very long and for that we\'re gonna use elipssisisisede + user name 2? + SCAM \ No newline at end of file