add status details

This commit is contained in:
Conny Duck 2020-06-29 21:32:49 +02:00
parent 47afa8818f
commit c5def59621
14 changed files with 550 additions and 67 deletions

View File

@ -48,6 +48,7 @@
<activity android:name=".components.about.licenses.LicenseActivity"/>
<activity android:name="at.connyduck.pixelcat.components.compose.ComposeActivity" />
<activity android:name=".components.profile.ProfileActivity" />
<activity android:name=".components.timeline.detail.DetailActivity" />
<service android:name=".components.compose.SendStatusService" />

View File

@ -27,6 +27,7 @@ import androidx.paging.ExperimentalPagingApi
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.SimpleItemAnimator
import at.connyduck.pixelcat.R
import at.connyduck.pixelcat.components.timeline.detail.DetailActivity
import at.connyduck.pixelcat.components.util.extension.getDisplayWidthInPx
import at.connyduck.pixelcat.components.util.getColorForAttr
import at.connyduck.pixelcat.dagger.ViewModelFactory
@ -101,4 +102,8 @@ class TimelineFragment : DaggerFragment(R.layout.fragment_timeline), TimeLineAct
override fun onMediaVisibilityChanged(status: StatusEntity) {
viewModel.onMediaVisibilityChanged(status)
}
override fun onDetailsOpened(status: StatusEntity) {
startActivity(DetailActivity.newIntent(requireContext(), status.actionableId))
}
}

View File

@ -31,6 +31,7 @@ import at.connyduck.pixelcat.databinding.ItemStatusBinding
import at.connyduck.pixelcat.db.entitity.StatusEntity
import coil.api.load
import coil.transform.RoundedCornersTransformation
import java.text.DateFormat
import java.text.SimpleDateFormat
interface TimeLineActionListener {
@ -38,6 +39,7 @@ interface TimeLineActionListener {
fun onBoost(post: StatusEntity)
fun onReply(status: StatusEntity)
fun onMediaVisibilityChanged(status: StatusEntity)
fun onDetailsOpened(status: StatusEntity)
}
object TimelineDiffUtil : DiffUtil.ItemCallback<StatusEntity>() {
@ -66,65 +68,71 @@ class TimelineListAdapter(
}
override fun onBindViewHolder(holder: BindingHolder<ItemStatusBinding>, position: Int) {
getItem(position)?.let { status ->
// TODO order the stuff here
(holder.binding.postImages.adapter as TimelineImageAdapter).images = status.attachments
val maxImageRatio = status.attachments.map {
if(it.meta?.small?.width == null || it.meta.small.height == null) {
1f
} else {
it.meta.small.height.toFloat() / it.meta.small.width.toFloat()
}
}.max()?.coerceAtMost(1f) ?: 1f
holder.binding.postImages.layoutParams.height = (displayWidth * maxImageRatio).toInt()
holder.binding.postAvatar.load(status.account.avatar) {
transformations(RoundedCornersTransformation(25f))
}
holder.binding.postAvatar.setOnClickListener {
holder.binding.root.context.startActivity(ProfileActivity.newIntent(holder.binding.root.context, status.account.id))
}
holder.binding.postDisplayName.text = status.account.displayName
holder.binding.postName.text = "@${status.account.username}"
holder.binding.postLikeButton.isChecked = status.favourited
holder.binding.postLikeButton.setEventListener { _, _ ->
listener.onFavorite(status)
true
}
holder.binding.postBoostButton.isChecked = status.reblogged
holder.binding.postBoostButton.setEventListener { _, _ ->
listener.onBoost(status)
true
}
holder.binding.postReplyButton.setOnClickListener {
listener.onReply(status)
}
holder.binding.postIndicator.visible = status.attachments.size > 1
holder.binding.postImages.visible = status.attachments.isNotEmpty()
holder.binding.postDescription.text = status.content.parseAsHtml().trim()
holder.binding.postDate.text = dateTimeFormatter.format(status.createdAt)
holder.binding.postSensitiveMediaOverlay.visible = status.attachments.isNotEmpty() && !status.mediaVisible
holder.binding.postSensitiveMediaOverlay.setOnClickListener {
listener.onMediaVisibilityChanged(status)
}
holder.bind(status, displayWidth, listener, dateTimeFormatter)
}
}
}
fun BindingHolder<ItemStatusBinding>.bind(status: StatusEntity, displayWidth: Int, listener: TimeLineActionListener, dateTimeFormatter: DateFormat) {
// TODO order the stuff here
(binding.postImages.adapter as TimelineImageAdapter).images = status.attachments
val maxImageRatio = status.attachments.map {
if(it.meta?.small?.width == null || it.meta.small.height == null) {
1f
} else {
it.meta.small.height.toFloat() / it.meta.small.width.toFloat()
}
}.max()?.coerceAtMost(1f) ?: 1f
binding.postImages.layoutParams.height = (displayWidth * maxImageRatio).toInt()
binding.postAvatar.load(status.account.avatar) {
transformations(RoundedCornersTransformation(25f))
}
binding.postAvatar.setOnClickListener {
binding.root.context.startActivity(ProfileActivity.newIntent(binding.root.context, status.account.id))
}
binding.postDisplayName.text = status.account.displayName
binding.postName.text = "@${status.account.username}"
binding.postLikeButton.isChecked = status.favourited
binding.postLikeButton.setEventListener { _, _ ->
listener.onFavorite(status)
true
}
binding.postBoostButton.isChecked = status.reblogged
binding.postBoostButton.setEventListener { _, _ ->
listener.onBoost(status)
true
}
binding.postReplyButton.setOnClickListener {
listener.onReply(status)
}
binding.postIndicator.visible = status.attachments.size > 1
binding.postImages.visible = status.attachments.isNotEmpty()
binding.postDescription.text = status.content.parseAsHtml().trim()
binding.postDate.text = dateTimeFormatter.format(status.createdAt)
binding.postSensitiveMediaOverlay.visible = status.attachments.isNotEmpty() && !status.mediaVisible
binding.postSensitiveMediaOverlay.setOnClickListener {
listener.onMediaVisibilityChanged(status)
}
binding.root.setOnClickListener {
listener.onDetailsOpened(status)
}
}

View File

@ -0,0 +1,102 @@
package at.connyduck.pixelcat.components.timeline.detail
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.MergeAdapter
import at.connyduck.pixelcat.components.general.BaseActivity
import at.connyduck.pixelcat.components.timeline.TimeLineActionListener
import at.connyduck.pixelcat.components.util.Success
import at.connyduck.pixelcat.components.util.extension.getDisplayWidthInPx
import at.connyduck.pixelcat.components.util.extension.hide
import at.connyduck.pixelcat.components.util.extension.show
import at.connyduck.pixelcat.dagger.ViewModelFactory
import at.connyduck.pixelcat.databinding.ActivityDetailBinding
import at.connyduck.pixelcat.db.entitity.StatusEntity
import at.connyduck.pixelcat.util.viewBinding
import javax.inject.Inject
class DetailActivity: BaseActivity(), TimeLineActionListener {
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val viewModel: DetailViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(ActivityDetailBinding::inflate)
private lateinit var statusAdapter: DetailStatusAdapter
private lateinit var repliesAdapter: DetailReplyAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
binding.root.setOnApplyWindowInsetsListener { _, insets ->
val top = insets.systemWindowInsetTop
binding.root.setPadding(0, top, 0, 0)
insets.consumeSystemWindowInsets()
}
viewModel.setStatusId(intent.getStringExtra(EXTRA_STATUS_ID)!!)
val displayWidth = getDisplayWidthInPx()
statusAdapter = DetailStatusAdapter(displayWidth, this)
repliesAdapter = DetailReplyAdapter(this)
binding.detailRecyclerView.adapter = MergeAdapter(statusAdapter, repliesAdapter)
viewModel.currentStatus.observe(this, Observer {
if(it is Success) {
binding.detailProgress.hide()
binding.detailRecyclerView.show()
statusAdapter.submitList(listOf(it.data))
}
})
viewModel.replies.observe(this, Observer {
if(it is Success) {
repliesAdapter.submitList(it.data)
}
})
}
override fun onFavorite(post: StatusEntity) {
TODO("Not yet implemented")
}
override fun onBoost(post: StatusEntity) {
TODO("Not yet implemented")
}
override fun onReply(status: StatusEntity) {
TODO("Not yet implemented")
}
override fun onMediaVisibilityChanged(status: StatusEntity) {
TODO("Not yet implemented")
}
override fun onDetailsOpened(status: StatusEntity) {
// nothing to do, we already are in details
}
companion object {
private const val EXTRA_STATUS_ID = "STATUS_ID"
fun newIntent(context: Context, statusId: String): Intent {
return Intent(context, DetailActivity::class.java).apply {
putExtra(EXTRA_STATUS_ID, statusId)
}
}
}
}

View File

@ -0,0 +1,58 @@
package at.connyduck.pixelcat.components.timeline.detail
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.ListAdapter
import at.connyduck.pixelcat.components.timeline.TimeLineActionListener
import at.connyduck.pixelcat.components.timeline.TimelineDiffUtil
import at.connyduck.pixelcat.components.util.BindingHolder
import at.connyduck.pixelcat.databinding.ItemReplyBinding
import at.connyduck.pixelcat.db.entitity.StatusEntity
import coil.api.load
import coil.transform.RoundedCornersTransformation
import java.text.SimpleDateFormat
class DetailReplyAdapter(
private val listener: TimeLineActionListener
): ListAdapter<StatusEntity, BindingHolder<ItemReplyBinding>>(TimelineDiffUtil) {
private val dateTimeFormatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemReplyBinding> {
val binding = ItemReplyBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding)
}
override fun onBindViewHolder(holder: BindingHolder<ItemReplyBinding>, position: Int) {
getItem(position)?.let { status ->
holder.binding.postAvatar.load(status.account.avatar) {
transformations(RoundedCornersTransformation(25f))
}
holder.binding.postDisplayName.text = status.account.displayName
holder.binding.postName.text = "@${status.account.username}"
holder.binding.postDescription.text = status.content.parseAsHtml().trim()
holder.binding.postDate.text = dateTimeFormatter.format(status.createdAt)
holder.binding.postLikeButton.isChecked = status.favourited
holder.binding.postLikeButton.setEventListener { _, _ ->
listener.onFavorite(status)
true
}
holder.binding.postReplyButton.setOnClickListener {
listener.onReply(status)
}
}
}
}

View File

@ -0,0 +1,40 @@
package at.connyduck.pixelcat.components.timeline.detail
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import at.connyduck.pixelcat.components.timeline.TimeLineActionListener
import at.connyduck.pixelcat.components.timeline.TimelineDiffUtil
import at.connyduck.pixelcat.components.timeline.TimelineImageAdapter
import at.connyduck.pixelcat.components.timeline.bind
import at.connyduck.pixelcat.components.util.BindingHolder
import at.connyduck.pixelcat.databinding.ItemStatusBinding
import at.connyduck.pixelcat.db.entitity.StatusEntity
import java.text.SimpleDateFormat
class DetailStatusAdapter(
private val displayWidth: Int,
private val listener: TimeLineActionListener
): ListAdapter<StatusEntity, BindingHolder<ItemStatusBinding>>(TimelineDiffUtil) {
private val dateTimeFormatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemStatusBinding> {
val binding = ItemStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
binding.postImages.adapter = TimelineImageAdapter()
binding.postIndicator.setViewPager(binding.postImages)
(binding.postImages.adapter as TimelineImageAdapter).registerAdapterDataObserver(binding.postIndicator.adapterDataObserver)
return BindingHolder(binding)
}
override fun onBindViewHolder(holder: BindingHolder<ItemStatusBinding>, position: Int) {
getItem(position)?.let { status ->
holder.bind(status, displayWidth, listener, dateTimeFormatter)
}
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright (C) 2020 Conny Duck
*
* This file is part of Pixelcat.
*
* Pixelcat is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pixelcat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package at.connyduck.pixelcat.components.timeline.detail
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.connyduck.pixelcat.components.util.Loading
import at.connyduck.pixelcat.components.util.Success
import at.connyduck.pixelcat.components.util.UiState
import at.connyduck.pixelcat.components.util.Error
import at.connyduck.pixelcat.db.AccountManager
import at.connyduck.pixelcat.db.AppDatabase
import at.connyduck.pixelcat.db.entitity.StatusEntity
import at.connyduck.pixelcat.db.entitity.toEntity
import at.connyduck.pixelcat.network.FediverseApi
import kotlinx.coroutines.launch
import javax.inject.Inject
class DetailViewModel @Inject constructor(
val api: FediverseApi,
val db: AppDatabase,
val accountManager: AccountManager
) : ViewModel() {
val currentStatus = MutableLiveData<UiState<StatusEntity>>()
val replies = MutableLiveData<UiState<List<StatusEntity>>>()
private var statusId = ""
fun setStatusId(statusId: String) {
this.statusId = statusId
currentStatus.value = Loading()
replies.value = Loading()
viewModelScope.launch {
db.statusDao().status(statusId, accountManager.activeAccount()?.id!!)?.let {
currentStatus.value = Success(it)
}
loadStatus()
}
viewModelScope.launch {
loadReplies()
}
}
fun reload() {
currentStatus.value = Loading()
replies.value = Loading()
viewModelScope.launch {
loadStatus()
}
viewModelScope.launch {
loadReplies()
}
}
private suspend fun loadStatus() {
api.status(statusId).fold({
val statusEntity = it.toEntity(accountManager.activeAccount()?.id!!)
db.statusDao().insertOrReplace(statusEntity)
currentStatus.value = Success(statusEntity)
}, {
currentStatus.value = Error(cause = it)
})
}
private suspend fun loadReplies() {
api.statusContext(statusId).fold({
replies.value = Success(it.descendants.map{
descendant -> descendant.toEntity(accountManager.activeAccount()?.id!!)
})
}, {
replies.value = Error(cause = it)
})
}
}

View File

@ -23,6 +23,7 @@ import at.connyduck.pixelcat.components.main.MainActivity
import at.connyduck.pixelcat.components.about.AboutActivity
import at.connyduck.pixelcat.components.about.licenses.LicenseActivity
import at.connyduck.pixelcat.components.compose.ComposeActivity
import at.connyduck.pixelcat.components.timeline.detail.DetailActivity
import at.connyduck.pixelcat.components.login.LoginActivity
import at.connyduck.pixelcat.components.profile.ProfileActivity
import at.connyduck.pixelcat.components.settings.SettingsActivity
@ -57,4 +58,7 @@ abstract class ActivityModule {
@ContributesAndroidInjector
abstract fun contributesComposeActivity(): ComposeActivity
@ContributesAndroidInjector
abstract fun contributesDetailActivity(): DetailActivity
}

View File

@ -17,13 +17,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// from https://proandroiddev.com/viewmodel-with-dagger2-architecture-components-2e06f06c9455
package at.connyduck.pixelcat.dagger
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import at.connyduck.pixelcat.components.compose.ComposeViewModel
import at.connyduck.pixelcat.components.timeline.detail.DetailViewModel
import at.connyduck.pixelcat.components.login.LoginViewModel
import at.connyduck.pixelcat.components.main.MainViewModel
import at.connyduck.pixelcat.components.notifications.NotificationsViewModel
@ -90,5 +89,10 @@ abstract class ViewModelModule {
@IntoMap
@ViewModelKey(ComposeViewModel::class)
internal abstract fun composeViewModel(viewModel: ComposeViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(DetailViewModel::class)
internal abstract fun detailViewModel(viewModel: DetailViewModel): ViewModel
// Add more ViewModels here
}

View File

@ -39,6 +39,9 @@ interface TimelineDao {
@Delete
suspend fun delete(status: StatusEntity)
@Query("SELECT * FROM StatusEntity WHERE id = :statusId AND accountId = :accountId")
suspend fun status(statusId: String, accountId: Long): StatusEntity?
@Query("SELECT * FROM StatusEntity WHERE accountId = :accountId ORDER BY LENGTH(id) DESC, id DESC")
fun statuses(accountId: Long): PagingSource<Int, StatusEntity>

View File

@ -0,0 +1,9 @@
package at.connyduck.pixelcat.model
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class StatusContext (
val ancestors: List<Status>,
val descendants: List<Status>
)

View File

@ -19,13 +19,7 @@
package at.connyduck.pixelcat.network
import at.connyduck.pixelcat.model.AccessToken
import at.connyduck.pixelcat.model.Account
import at.connyduck.pixelcat.model.AppCredentials
import at.connyduck.pixelcat.model.Attachment
import at.connyduck.pixelcat.model.NewStatus
import at.connyduck.pixelcat.model.Relationship
import at.connyduck.pixelcat.model.Status
import at.connyduck.pixelcat.model.*
import at.connyduck.pixelcat.network.calladapter.NetworkResponse
import okhttp3.MultipartBody
import retrofit2.http.Body
@ -183,4 +177,14 @@ interface FediverseApi {
suspend fun unreblogStatus(
@Path("id") statusId: String
): NetworkResponse<Status>
@GET("api/v1/statuses/{id}")
suspend fun status(
@Path("id") statusId: String
): NetworkResponse<Status>
@GET("api/v1/statuses/{id}/context")
suspend fun statusContext(
@Path("id") statusId: String
): NetworkResponse<StatusContext>
}

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/pixelcat_gradient">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/detailAppbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/detailToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationIcon="@drawable/ic_cat_small"
app:title="@string/app_name"
app:titleTextAppearance="@style/TextAppearanceToolbar"
app:titleTextColor="?attr/colorPrimary" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/detailSwipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/windowBackground"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/detailRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<ProgressBar
android:id="@+id/detailProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/postAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:importantForAccessibility="no"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="#f00" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/postDisplayName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:lines="1"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/postName"
app:layout_constraintStart_toEndOf="@id/postAvatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="Conny Duck" />
<TextView
android:id="@+id/postName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:lines="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/postDisplayName"
app:layout_constraintTop_toTopOf="parent"
tools:text="\@connyduck\@chaos.social" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/postDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toEndOf="@+id/postAvatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/postDisplayName"
tools:text="This is the caption" />
<TextView
android:id="@+id/postDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:textSize="12sp"
app:layout_constraintStart_toEndOf="@+id/postAvatar"
app:layout_constraintTop_toBottomOf="@id/postDescription"
tools:text="4 hours ago" />
<at.connyduck.sparkbutton.SparkButton
android:id="@+id/postLikeButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toEndOf="@id/postDate"
app:layout_constraintTop_toBottomOf="@id/postDescription"
sparkbutton:activeImage="@drawable/ic_heart_filled"
sparkbutton:iconSize="24dp"
sparkbutton:inactiveImage="@drawable/ic_heart"
sparkbutton:primaryColor="@color/heart_button_primary"
sparkbutton:secondaryColor="@color/heart_button_secondary" />
<ImageButton
android:id="@+id/postReplyButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_message"
app:layout_constraintStart_toEndOf="@id/postLikeButton"
app:layout_constraintTop_toBottomOf="@id/postDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>