Skip to content

Commit 9aa51a3

Browse files
Merge pull request #39 from LebedevSergeyVach/develop
Develop to Main. Implementation of the functionality of comments on posts. Version 2.0.0 16.04.2025.
2 parents 17b9acc + 3b4509f commit 9aa51a3

File tree

73 files changed

+2072
-208
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+2072
-208
lines changed

README.md

+24-10
Large diffs are not rendered by default.

app/build.gradle.kts

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ android {
4141

4242
defaultConfig {
4343
applicationId = "com.eltex.androidschool.social.media.network"
44-
minSdk = 24
44+
minSdk = 26
4545
targetSdk = 36
4646
versionCode = 1
47-
versionName = "v1.1.2"
47+
versionName = "v2.0.0"
4848

4949
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
5050

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.eltex.androidschool.adapter.comments
2+
3+
import androidx.recyclerview.widget.DiffUtil
4+
5+
import com.eltex.androidschool.ui.comments.CommentUiModel
6+
7+
class CommentItemCallback : DiffUtil.ItemCallback<CommentUiModel>() {
8+
9+
override fun areItemsTheSame(oldItem: CommentUiModel, newItem: CommentUiModel): Boolean =
10+
oldItem.id == newItem.id
11+
12+
override fun areContentsTheSame(oldItem: CommentUiModel, newItem: CommentUiModel): Boolean =
13+
oldItem == newItem
14+
15+
override fun getChangePayload(oldItem: CommentUiModel, newItem: CommentUiModel): Any? =
16+
CommentPayload(
17+
likeByMe = newItem.likedByMe.takeIf { likeByMe: Boolean ->
18+
likeByMe != oldItem.likedByMe
19+
},
20+
likes = newItem.likes.takeIf { likes: Int ->
21+
likes != oldItem.likes
22+
},
23+
)
24+
.takeIf { commentPayload: CommentPayload ->
25+
commentPayload.isNotEmpty()
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.eltex.androidschool.adapter.comments
2+
3+
data class CommentPayload(
4+
val likeByMe: Boolean? = null,
5+
val likes: Int? = null,
6+
) {
7+
8+
fun isNotEmpty(): Boolean = likeByMe != null || likes != null
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package com.eltex.androidschool.adapter.comments
2+
3+
import android.annotation.SuppressLint
4+
5+
import android.graphics.drawable.Drawable
6+
import android.view.MotionEvent
7+
8+
import androidx.core.content.ContextCompat
9+
import androidx.core.view.isVisible
10+
11+
import androidx.recyclerview.widget.RecyclerView.ViewHolder
12+
13+
import com.bumptech.glide.Glide
14+
import com.bumptech.glide.load.DataSource
15+
import com.bumptech.glide.load.engine.DiskCacheStrategy
16+
import com.bumptech.glide.load.engine.GlideException
17+
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
18+
import com.bumptech.glide.request.RequestListener
19+
import com.bumptech.glide.request.target.Target
20+
21+
import com.eltex.androidschool.R
22+
import com.eltex.androidschool.databinding.CardCommentBinding
23+
import com.eltex.androidschool.ui.comments.CommentUiModel
24+
import com.eltex.androidschool.utils.common.initialsOfUsername
25+
import com.eltex.androidschool.utils.extensions.buttonClickAnimationScale
26+
27+
@SuppressLint("ClickableViewAccessibility")
28+
class CommentViewHolder(
29+
private val binding: CardCommentBinding,
30+
) : ViewHolder(binding.root) {
31+
private var lastClickTime: Long = 0
32+
33+
init {
34+
binding.cardComment.setOnTouchListener { _, comment: MotionEvent ->
35+
if (comment.action == MotionEvent.ACTION_DOWN) {
36+
val clickTime = System.currentTimeMillis()
37+
if (clickTime - lastClickTime < 300) {
38+
onDoubleClick()
39+
}
40+
lastClickTime = clickTime
41+
}
42+
false
43+
}
44+
}
45+
46+
private fun onDoubleClick() {
47+
binding.like.performClick()
48+
}
49+
50+
fun bindComment(comment: CommentUiModel, currentUserId: Long) {
51+
binding.author.text = comment.author
52+
binding.published.text = comment.published
53+
binding.content.text = comment.content
54+
binding.like.text = comment.likes.toString()
55+
56+
binding.menu.isVisible = comment.authorId == currentUserId
57+
58+
updateLike(likeByMe = comment.likedByMe)
59+
renderingUserAvatar(comment = comment)
60+
}
61+
62+
fun bindPayload(payload: CommentPayload) {
63+
payload.likeByMe?.let { likeByMe: Boolean ->
64+
updateLike(likeByMe)
65+
66+
binding.root.context.buttonClickAnimationScale(
67+
button = binding.like,
68+
condition = likeByMe,
69+
causeVibration = true
70+
)
71+
}
72+
73+
payload.likes?.let { likes: Int ->
74+
binding.like.text = likes.toString()
75+
}
76+
}
77+
78+
private fun updateLike(likeByMe: Boolean) {
79+
binding.like.isSelected = likeByMe
80+
}
81+
82+
fun renderingUserAvatar(comment: CommentUiModel) {
83+
showPlaceholder(comment = comment)
84+
binding.skeletonLayout.showSkeleton()
85+
86+
if (!comment.authorAvatar.isNullOrEmpty()) {
87+
Glide.with(binding.root)
88+
.load(comment.authorAvatar)
89+
.circleCrop()
90+
.diskCacheStrategy(DiskCacheStrategy.ALL)
91+
.listener(object : RequestListener<Drawable> {
92+
override fun onLoadFailed(
93+
e: GlideException?,
94+
model: Any?,
95+
target: Target<Drawable>,
96+
isFirstResource: Boolean
97+
): Boolean {
98+
showPlaceholder(comment = comment)
99+
100+
return false
101+
}
102+
103+
override fun onResourceReady(
104+
resource: Drawable,
105+
model: Any,
106+
target: Target<Drawable>?,
107+
dataSource: DataSource,
108+
isFirstResource: Boolean
109+
): Boolean {
110+
binding.skeletonLayout.showOriginal()
111+
binding.initial.isVisible = false
112+
113+
return false
114+
}
115+
})
116+
.transition(DrawableTransitionOptions.withCrossFade(500))
117+
.error(R.drawable.error_placeholder)
118+
.thumbnail(
119+
Glide.with(binding.root)
120+
.load(comment.authorAvatar)
121+
.override(50, 50)
122+
.circleCrop()
123+
.diskCacheStrategy(DiskCacheStrategy.ALL)
124+
)
125+
.into(binding.avatar)
126+
} else {
127+
showPlaceholder(comment = comment)
128+
}
129+
}
130+
131+
private fun showPlaceholder(comment: CommentUiModel) {
132+
binding.avatar.setImageResource(R.drawable.avatar_background)
133+
binding.initial.text = initialsOfUsername(name = comment.author)
134+
binding.initial.setTextColor(ContextCompat.getColor(binding.root.context, R.color.white))
135+
binding.skeletonLayout.showOriginal()
136+
binding.initial.isVisible = true
137+
}
138+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package com.eltex.androidschool.adapter.comments
2+
3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.graphics.drawable.InsetDrawable
6+
import android.os.Build
7+
import android.util.TypedValue
8+
import android.view.LayoutInflater
9+
import android.view.MenuItem
10+
import android.view.View
11+
import android.view.ViewGroup
12+
import androidx.appcompat.view.menu.MenuBuilder
13+
import androidx.appcompat.widget.PopupMenu
14+
import androidx.recyclerview.widget.ListAdapter
15+
import com.eltex.androidschool.BuildConfig
16+
import com.eltex.androidschool.R
17+
import com.eltex.androidschool.databinding.CardCommentBinding
18+
import com.eltex.androidschool.ui.comments.CommentUiModel
19+
import com.eltex.androidschool.utils.extensions.singleVibrationWithSystemCheck
20+
import com.eltex.androidschool.utils.helper.LoggerHelper
21+
22+
class CommentsAdapter(
23+
private val listener: CommentListener,
24+
private val context: Context,
25+
private val currentUserId: Long,
26+
) : ListAdapter<CommentUiModel, CommentViewHolder>(CommentItemCallback()) {
27+
28+
interface CommentListener {
29+
fun onLikeClicked(comment: CommentUiModel)
30+
fun onDeleteClicked(comment: CommentUiModel)
31+
fun onGetUserClicked(comment: CommentUiModel)
32+
}
33+
34+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder {
35+
val binding = CardCommentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
36+
37+
val viewHolder = CommentViewHolder(binding = binding)
38+
39+
binding.like.setOnClickListener {
40+
listener.onLikeClicked(getItem(viewHolder.bindingAdapterPosition))
41+
}
42+
43+
binding.avatar.setOnClickListener {
44+
listener.onGetUserClicked(getItem(viewHolder.bindingAdapterPosition))
45+
}
46+
47+
binding.author.setOnClickListener {
48+
listener.onGetUserClicked(getItem(viewHolder.bindingAdapterPosition))
49+
}
50+
51+
binding.menu.setOnClickListener { view: View ->
52+
showPopupMenu(view = view, position = viewHolder.bindingAdapterPosition)
53+
}
54+
55+
return viewHolder
56+
}
57+
58+
override fun onBindViewHolder(holder: CommentViewHolder, position: Int) {
59+
holder.bindComment(
60+
comment = getItem(position),
61+
currentUserId = currentUserId
62+
)
63+
}
64+
65+
66+
override fun onBindViewHolder(holder: CommentViewHolder, position: Int, payloads: List<Any?>) {
67+
if (payloads.isNotEmpty()) {
68+
payloads.forEach { comment: Any? ->
69+
if (comment is CommentPayload) {
70+
holder.bindPayload(payload = comment)
71+
}
72+
}
73+
} else {
74+
onBindViewHolder(holder = holder, position = position)
75+
}
76+
}
77+
78+
@SuppressLint("RestrictedApi", "ObsoleteSdkInt")
79+
private fun showPopupMenu(view: View, position: Int) {
80+
context.singleVibrationWithSystemCheck(35)
81+
82+
val resources = view.context.resources
83+
84+
val iconMarginPx = TypedValue.applyDimension(
85+
TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics
86+
).toInt()
87+
88+
val popup = PopupMenu(view.context, view).apply {
89+
inflate(R.menu.menu_comment)
90+
91+
if (menu is MenuBuilder) {
92+
val menuBuilder = menu as MenuBuilder
93+
menuBuilder.setOptionalIconsVisible(true)
94+
95+
for (item in menuBuilder.visibleItems) {
96+
if (item.icon != null) {
97+
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
98+
item.icon = InsetDrawable(item.icon, iconMarginPx, 0, iconMarginPx, 0)
99+
} else {
100+
item.icon = object :
101+
InsetDrawable(item.icon, iconMarginPx, 0, iconMarginPx, 0) {
102+
override fun getIntrinsicWidth(): Int {
103+
return intrinsicHeight + iconMarginPx + iconMarginPx
104+
}
105+
}
106+
}
107+
}
108+
}
109+
}
110+
111+
setOnMenuItemClickListener { menuItem: MenuItem ->
112+
when (menuItem.itemId) {
113+
R.id.delete_comment -> {
114+
listener.onDeleteClicked(getItem(position))
115+
context.singleVibrationWithSystemCheck(35)
116+
117+
true
118+
}
119+
120+
else -> {
121+
false
122+
}
123+
}
124+
}
125+
}
126+
127+
popup.show()
128+
}
129+
}

app/src/main/java/com/eltex/androidschool/adapter/events/EventAdapter.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class EventAdapter(
8989
}
9090

9191
binding.menu.setOnClickListener { view: View ->
92-
showPopupMenu(view, viewHolder.bindingAdapterPosition)
92+
showPopupMenu(view = view, position = viewHolder.bindingAdapterPosition)
9393
}
9494

9595
return viewHolder

app/src/main/java/com/eltex/androidschool/adapter/events/EventPayload.kt

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ data class EventPayload(
1616
val participateByMe: Boolean? = null,
1717
val participates: Int? = null,
1818
) {
19+
1920
/**
2021
* Проверяет, есть ли изменения в объекте.
2122
*

app/src/main/java/com/eltex/androidschool/adapter/events/EventViewHolder.kt

-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ class EventViewHolder(
7474
*
7575
* @param event Событие, данные которого нужно отобразить.
7676
*/
77-
@SuppressLint("SetTextI18n")
7877
fun bindEvent(event: EventUiModel, currentUserId: Long) {
7978
binding.author.text = event.author
8079
binding.published.text = event.published
@@ -298,7 +297,6 @@ class EventViewHolder(
298297
*
299298
* @param payload Изменения в событии.
300299
*/
301-
@SuppressLint("SetTextI18n")
302300
fun bind(payload: EventPayload) {
303301
payload.likeByMe?.let { likeByMe: Boolean ->
304302
updateLike(likeByMe)

app/src/main/java/com/eltex/androidschool/adapter/posts/PostAdapter.kt

+8
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class PostAdapter(
6666
fun onDeleteClicked(post: PostUiModel)
6767
fun onUpdateClicked(post: PostUiModel)
6868
fun onGetUserClicked(post: PostUiModel)
69+
fun onCommentsClicked(post: PostUiModel)
6970
fun onRetryPageClicked()
7071
}
7172

@@ -193,6 +194,13 @@ class PostAdapter(
193194
showPopupMenu(view = view, position = viewHolder.bindingAdapterPosition)
194195
}
195196

197+
binding.comments.setOnClickListener {
198+
val item: PagingModel.Data<PostUiModel>? =
199+
getItem(viewHolder.bindingAdapterPosition) as? PagingModel.Data
200+
201+
item?.value?.let(listener::onCommentsClicked)
202+
}
203+
196204
return viewHolder
197205
}
198206

app/src/main/java/com/eltex/androidschool/adapter/posts/PostViewHolder.kt

-2
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ class PostViewHolder(
7979
* @param post Пост, данные которого нужно отобразить.
8080
* @param currentUserId ID текущего пользователя для определения прав на редактирование поста.
8181
*/
82-
@SuppressLint("SetTextI18n")
8382
fun bindPost(post: PostUiModel, currentUserId: Long) {
8483
binding.author.text = post.author
8584
binding.content.text = post.content
@@ -280,7 +279,6 @@ class PostViewHolder(
280279
*
281280
* @param payload Изменения в посте.
282281
*/
283-
@SuppressLint("SetTextI18n")
284282
fun bind(payload: PostPayload) {
285283
payload.likeByMe?.let { likeByMe: Boolean ->
286284
updateLike(likeByMe)

app/src/main/java/com/eltex/androidschool/adapter/users/UserPagerAdapter.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import com.eltex.androidschool.adapter.posts.PostAdapter
1313
import com.eltex.androidschool.databinding.LayoutPostListBinding
1414
import com.eltex.androidschool.databinding.LayoutEventListBinding
1515
import com.eltex.androidschool.databinding.LayoutJobListBinding
16-
import com.eltex.androidschool.ui.common.OffsetDecoration
16+
import com.eltex.androidschool.ui.offset.OffsetDecoration
1717
import com.eltex.androidschool.viewmodel.posts.postswall.PostWallViewModel
1818
import com.eltex.androidschool.viewmodel.posts.postswall.PostWallMessage
1919

0 commit comments

Comments
 (0)