Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 12 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8


}
kotlinOptions {
jvmTarget = '1.8'
}

viewBinding {
enabled = true
}
}

dependencies {
Expand All @@ -38,7 +44,11 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
//implementation 'com.github.telegram:telegram-design-system:7.8.1'
implementation 'com.github.bumptech.glide:glide:4.15.1'
//kapt 'com.github.bumptech.glide:compiler:4.15.1' // For annotation processing if required testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
}


8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand All @@ -21,6 +25,10 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>




</manifest>
138 changes: 138 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/Chat.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package otus.gpb.recyclerview

interface Chat {
override fun equals(other: Any?): Boolean
}

// Parent interface
interface DataChat: Chat {
val id: Int
val lastUserName: String
val lastMessage: String
val time: String
val isVerified: Boolean
val isScam: Boolean
val unreadMessageCount: Int
val avatarUrl: String?
val lastAnswererUrl: String?
val isPinned: Boolean
var isMute: Boolean
var isSpeeching: Boolean
var isTyping: Boolean
var isUnreadedAnswerToYou: Boolean
var isAnswered: Boolean
var isOpponnentReaded: Boolean
var isLock: Boolean

}

// Child class for group chat
data class GroupChat(
override val id: Int,
val title: String,
override val lastUserName: String,
override val lastMessage: String,
override val time: String,
override val isVerified: Boolean = true,
override val isScam: Boolean = false,
override val unreadMessageCount: Int = 0,
override val avatarUrl: String? = null,
override val lastAnswererUrl: String? = null,
override val isPinned: Boolean = false,
override var isMute: Boolean = false,
override var isSpeeching: Boolean = false,
override var isTyping: Boolean = false,
override var isUnreadedAnswerToYou: Boolean = false,
override var isAnswered: Boolean = false,
override var isOpponnentReaded: Boolean = false,
override var isLock: Boolean = false


) : DataChat {

// Custom equals implementation for GroupChat
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is GroupChat) return false
return id == other.id &&
lastUserName == other.lastUserName &&
lastMessage == other.lastMessage &&
time == other.time &&
isVerified == other.isVerified &&
isScam == other.isScam &&
unreadMessageCount == other.unreadMessageCount &&
avatarUrl == other.avatarUrl &&
lastAnswererUrl == other.lastAnswererUrl &&
isPinned == other.isPinned &&
isMute == other.isMute &&
isSpeeching == other.isSpeeching &&
isTyping == other.isTyping &&
isUnreadedAnswerToYou == other.isUnreadedAnswerToYou &&
isAnswered == other.isAnswered &&
isOpponnentReaded == other.isOpponnentReaded &&
isLock == other.isLock
}

override fun hashCode(): Int {
return id.hashCode()
}
}

// Child class for user chat
data class UserChat(
override val id: Int,
override val lastUserName: String,
override val lastMessage: String,
override val time: String,
override val isVerified: Boolean = true,
override val isScam: Boolean = false,
override val unreadMessageCount: Int = 0,
override val avatarUrl: String? = null,
override val lastAnswererUrl: String? = null,
override val isPinned: Boolean = false,
override var isMute: Boolean = false,
override var isSpeeching: Boolean = false,
override var isTyping: Boolean = false,
override var isUnreadedAnswerToYou: Boolean = false,
override var isAnswered: Boolean = false,
override var isOpponnentReaded: Boolean = false,
override var isLock: Boolean = false
) : DataChat {

// Custom equals implementation for UserChat
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is UserChat) return false
return id == other.id &&
lastUserName == other.lastUserName &&
lastMessage == other.lastMessage &&
time == other.time &&
isVerified == other.isVerified &&
isScam == other.isScam &&
unreadMessageCount == other.unreadMessageCount &&
avatarUrl == other.avatarUrl &&
lastAnswererUrl == other.lastAnswererUrl &&
isPinned == other.isPinned &&
isMute == other.isMute &&
isSpeeching == other.isSpeeching &&
isTyping == other.isTyping &&
isUnreadedAnswerToYou == other.isUnreadedAnswerToYou &&
isAnswered == other.isAnswered &&
isOpponnentReaded == other.isOpponnentReaded &&
isLock == other.isLock
}

override fun hashCode(): Int {
return id.hashCode()
}
}

object ChatLoading : Chat {
override fun equals(other: Any?): Boolean {
return this === other
}

override fun hashCode(): Int {
return javaClass.hashCode()
}
}
186 changes: 186 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package otus.gpb.recyclerview

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import otus.gpb.recyclerview.databinding.ItemChatBinding


class ChatAdapter(
private val context: Context,
private val onArchiveChat: (Chat) -> Unit // Новый callback для архивирования
) : ListAdapter<Chat, ViewHolder>(ChatDiffCallback()) {

companion object {
private const val VIEW_TYPE_DATA = 0
private const val VIEW_TYPE_LOADING = 1
}

override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is DataChat -> VIEW_TYPE_DATA
is ChatLoading -> VIEW_TYPE_LOADING
else -> throw IllegalArgumentException(context.getString(R.string.invalid_view_type))
}
}

inner class ChatViewHolder(private val binding: ItemChatBinding) :
ViewHolder(binding.root) {

fun bind(chat: Chat) {
if (chat is DataChat) {
// Установка данных в зависимости от типа чата
when (chat) {
is GroupChat -> {
binding.chatTopic.text = chat.title
binding.chatLastUser.text = chat.lastUserName
binding.chatLastUser.visibility = View.VISIBLE
}

is UserChat -> {
binding.chatTopic.text = chat.lastUserName
binding.chatLastUser.visibility = View.GONE
}
}

binding.chatLastMessage.text = chat.lastMessage
binding.chatTime.text = chat.time

// Отображение иконок статусов
binding.speechingStatusIcon.visibility =
if (chat.isSpeeching) View.VISIBLE else View.GONE
binding.muteChannelIcon.visibility = if (chat.isMute) View.VISIBLE else View.GONE

binding.lockIcon.visibility = if (chat.isLock) View.VISIBLE else View.GONE
binding.scamIcon.visibility = if (chat.isScam) View.VISIBLE else View.GONE

binding.verifiedIcon.visibility = if (chat.isVerified) View.VISIBLE else View.GONE

binding.unreadCount.visibility =
if (chat.unreadMessageCount > 0) View.VISIBLE else View.GONE
chat.unreadMessageCount.toString().also { binding.unreadCount.text = it }

binding.typingIcon.visibility = if (chat.isTyping) View.VISIBLE else View.GONE
binding.typing.visibility = if (chat.isTyping) View.VISIBLE else View.GONE
binding.chatLastMessage.visibility = if (chat.isTyping) View.GONE else View.VISIBLE

binding.answeredToYouStatus.visibility =
if (chat.isUnreadedAnswerToYou) View.VISIBLE else View.GONE

binding.answeredIcon.visibility =
if (chat.isAnswered && !chat.isOpponnentReaded) View.VISIBLE else View.GONE
binding.answeredAndReadedIcon.visibility =
if (chat.isAnswered && chat.isOpponnentReaded) View.VISIBLE else View.GONE
binding.pinnedStatus.visibility = if (chat.isPinned) View.VISIBLE else View.GONE

val targetSize = 500 // Размер в пикселях, до которого нужно увеличить изображение
// Загрузка аватара
if (chat.avatarUrl != null) {
Glide.with(binding.chatAvatar.context)
.load(chat.avatarUrl)
.transform(
ResizeAndCropTransformation(targetSize),
CircleCrop()
) // Обрезаем и применяем круг
.into(binding.chatAvatar)
} else {
binding.chatAvatar.setImageResource(R.drawable.ic_placeholder_avatar)
}

// Загрузка аватара ответившего
if (chat.lastAnswererUrl != null) {
Glide.with(binding.answererIcon.context)
.load(chat.lastAnswererUrl)
//.circleCrop()
.into(binding.answererIcon)
binding.answererIcon.visibility = View.VISIBLE
} else {
binding.answererIcon.setImageResource(android.R.color.transparent)
binding.answererIcon.visibility = View.GONE
}

// Обработчик нажатия на кнопку архивирования
binding.archiveButton.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val chatObj = currentList[position]
// Обработка архивации
onArchiveChat(chatObj)
// Скрыть шторку
val archiveLayout =
binding.root.findViewById<FrameLayout>(R.id.archiveButtonLayout)
archiveLayout.visibility = View.GONE
notifyItemChanged(position)
}
}


binding.root.setOnClickListener {
val archiveLayout =
binding.root.findViewById<FrameLayout>(R.id.archiveButtonLayout)
val itemLayout = binding.root.findViewById<View>(R.id.chatItemLayout)
if (archiveLayout.visibility == View.VISIBLE) {
// Анимация возврата в исходное положение
itemLayout.animate()
.translationX(0f)
.setDuration(500)
.withEndAction {
// После завершения анимации скрываем шторку
archiveLayout.visibility = View.GONE
// Обновляем элемент только после завершения анимации
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
notifyItemChanged(position)
}
}
.start()

}
}
}
}
}

inner class LoadingViewHolder(view: View) : ViewHolder(view)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return when (viewType) {
VIEW_TYPE_DATA -> {
val binding = ItemChatBinding.inflate(LayoutInflater.from(parent.context), parent, false)
ChatViewHolder(binding)
}
VIEW_TYPE_LOADING -> {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_loading, parent, false)
LoadingViewHolder(view)
}
else -> throw IllegalArgumentException(context.getString(R.string.invalid_view_type))
}
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
when (holder) {
is ChatViewHolder -> holder.bind(getItem(position) as DataChat)
is LoadingViewHolder -> {
// Здесь ничего не нужно делать, если загрузка не требует биндинга
}
else -> throw IllegalArgumentException(context.getString(R.string.invalid_view_type))
}
}

}

class ChatDiffCallback : DiffUtil.ItemCallback<Chat>() {
override fun areItemsTheSame(oldItem: Chat, newItem: Chat): Boolean
= (oldItem as? DataChat)?.id == (newItem as? DataChat)?.id
override fun areContentsTheSame(oldItem: Chat, newItem: Chat): Boolean
= oldItem == newItem
}
Loading