mirror of
https://gitlab.shinice.net/pixeldroid/PixelDroid
synced 2025-01-30 15:24:49 +01:00
WIP
This commit is contained in:
parent
0fe2c54940
commit
7f3ab1e4a6
@ -0,0 +1,262 @@
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds.directMessages
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentConversationsBinding
|
||||
import org.pixeldroid.app.databinding.FragmentNotificationsBinding
|
||||
import org.pixeldroid.app.posts.PostActivity
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory
|
||||
import org.pixeldroid.app.posts.parseHTMLText
|
||||
import org.pixeldroid.app.posts.setTextViewFromISO8601
|
||||
import org.pixeldroid.app.profile.ProfileActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.notificationsWorker.makeChannelGroupId
|
||||
|
||||
|
||||
/**
|
||||
* Fragment for the notifications tab.
|
||||
*/
|
||||
class DirectMessagesFragment : CachedFeedFragment<Conversation>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
adapter = DirectMessagesAdapter(apiHolder)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel = ViewModelProvider(
|
||||
requireActivity(),
|
||||
ViewModelFactory(db, db.directMessagesDao(), DirectMessagesRemoteMediator(apiHolder, db))
|
||||
)["conversations", FeedViewModel::class.java] as FeedViewModel<Conversation>
|
||||
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
with(NotificationManagerCompat.from(requireContext())) {
|
||||
// Cancel account notification group
|
||||
db.userDao().getActiveUser()?.let {
|
||||
cancel( makeChannelGroupId(it).hashCode())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View Holder for a [Conversation] RecyclerView list item.
|
||||
*/
|
||||
class ConversationViewHolder(binding: FragmentConversationsBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
private val messageTime: TextView = binding.messageTime
|
||||
private val avatar: ImageView = binding.notificationAvatar
|
||||
private val photoThumbnail: ImageView = binding.notificationPhotoThumbnail
|
||||
|
||||
private var conversation: Conversation? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
conversation?.openActivity()
|
||||
}
|
||||
avatar.setOnClickListener {
|
||||
val intent = conversation?.openAccountFromNotification()
|
||||
itemView.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Conversation.openActivity() {
|
||||
val intent: Intent = openConversation()
|
||||
itemView.context.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun Notification.openConversation(): Intent =
|
||||
Intent(itemView.context, PostActivity::class.java).apply {
|
||||
putExtra(Status.POST_TAG, status)
|
||||
}
|
||||
|
||||
private fun setNotificationType(
|
||||
type: Notification.NotificationType,
|
||||
username: String,
|
||||
textView: TextView
|
||||
) {
|
||||
val context = textView.context
|
||||
val (format: String, drawable: Drawable?) = when (type) {
|
||||
Notification.NotificationType.follow ->
|
||||
getStringAndDrawable(
|
||||
context,
|
||||
R.string.followed_notification,
|
||||
R.drawable.ic_follow
|
||||
)
|
||||
Notification.NotificationType.mention ->
|
||||
getStringAndDrawable(
|
||||
context,
|
||||
R.string.mention_notification,
|
||||
R.drawable.mention_at_24dp
|
||||
)
|
||||
Notification.NotificationType.comment ->
|
||||
getStringAndDrawable(
|
||||
context,
|
||||
R.string.comment_notification,
|
||||
R.drawable.ic_comment_empty
|
||||
)
|
||||
Notification.NotificationType.reblog ->
|
||||
getStringAndDrawable(
|
||||
context,
|
||||
R.string.shared_notification,
|
||||
R.drawable.ic_reblog_blue
|
||||
)
|
||||
Notification.NotificationType.favourite ->
|
||||
getStringAndDrawable(
|
||||
context,
|
||||
R.string.liked_notification,
|
||||
R.drawable.ic_like_full
|
||||
)
|
||||
Notification.NotificationType.poll ->
|
||||
getStringAndDrawable(context, R.string.poll_notification, R.drawable.poll)
|
||||
Notification.NotificationType.follow_request -> getStringAndDrawable(
|
||||
context,
|
||||
R.string.follow_request,
|
||||
R.drawable.ic_follow
|
||||
)
|
||||
Notification.NotificationType.status -> getStringAndDrawable(
|
||||
context,
|
||||
R.string.status_notification,
|
||||
R.drawable.ic_comment_empty
|
||||
)
|
||||
}
|
||||
textView.text = format.format(username)
|
||||
textView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
drawable, null, null, null
|
||||
)
|
||||
}
|
||||
|
||||
private fun getStringAndDrawable(
|
||||
context: Context,
|
||||
stringToFormat: Int,
|
||||
drawable: Int
|
||||
): Pair<String, Drawable?> =
|
||||
Pair(context.getString(stringToFormat), ContextCompat.getDrawable(context, drawable))
|
||||
|
||||
|
||||
fun bind(
|
||||
conversation: Conversation?,
|
||||
api: PixelfedAPIHolder,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
) {
|
||||
|
||||
this.conversation = conversation
|
||||
|
||||
Glide.with(itemView).load(conversation?.accounts?.first()?.anyAvatar()).circleCrop()
|
||||
.into(avatar)
|
||||
|
||||
val previewUrl = conversation?.accounts?.first()?.anyAvatar()
|
||||
if (!previewUrl.isNullOrBlank()) {
|
||||
Glide.with(itemView).load(previewUrl)
|
||||
.placeholder(R.drawable.ic_picture_fallback).into(photoThumbnail)
|
||||
} else {
|
||||
photoThumbnail.visibility = View.GONE
|
||||
}
|
||||
|
||||
conversation?.last_status?.created_at?.let {
|
||||
setTextViewFromISO8601(
|
||||
it,
|
||||
notificationTime,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
// Convert HTML to clickable text
|
||||
postDescription.text =
|
||||
parseHTMLText(
|
||||
notification?.status?.content ?: "",
|
||||
notification?.status?.mentions,
|
||||
api,
|
||||
itemView.context,
|
||||
lifecycleScope
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(parent: ViewGroup): ConversationViewHolder {
|
||||
val itemBinding = FragmentNotificationsBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return ConversationViewHolder(itemBinding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inner class DirectMessagesAdapter(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
) : PagingDataAdapter<Conversation, RecyclerView.ViewHolder>(
|
||||
object : DiffUtil.ItemCallback<Conversation>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: Conversation,
|
||||
newItem: Conversation
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: Conversation,
|
||||
newItem: Conversation
|
||||
): Boolean =
|
||||
oldItem == newItem
|
||||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return ConversationViewHolder.create(parent)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return R.layout.fragment_notifications
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val uiModel = getItem(position)
|
||||
uiModel?.let {
|
||||
(holder as ConversationViewHolder).bind(
|
||||
it,
|
||||
apiHolder,
|
||||
lifecycleScope
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds.directMessages
|
||||
|
||||
import androidx.paging.*
|
||||
import androidx.room.withTransaction
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import java.lang.Exception
|
||||
import java.lang.NullPointerException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* RemoteMediator for the notifications.
|
||||
*
|
||||
* A [RemoteMediator] defines a set of callbacks used to incrementally load data from a remote
|
||||
* source into a local source wrapped by a [PagingSource], e.g., loading data from network into
|
||||
* a local db cache.
|
||||
*/
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class DirectMessagesRemoteMediator @Inject constructor(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase
|
||||
) : RemoteMediator<Int, Conversation>() {
|
||||
|
||||
override suspend fun load(loadType: LoadType, state: PagingState<Int, Conversation>): MediatorResult {
|
||||
|
||||
val maxId = when (loadType) {
|
||||
LoadType.REFRESH -> null
|
||||
LoadType.PREPEND -> {
|
||||
// No prepend for the moment, might be nice to add later
|
||||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
LoadType.APPEND -> state.lastItemOrNull()?.id
|
||||
?: return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
|
||||
try {
|
||||
val user = db.userDao().getActiveUser()
|
||||
?: return MediatorResult.Error(NullPointerException("No active user exists"))
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
val apiResponse = api.viewAllConversations(
|
||||
max_id = maxId,
|
||||
limit = state.config.pageSize.toString()
|
||||
)
|
||||
|
||||
apiResponse.forEach{it.user_id = user.user_id; it.instance_uri = user.instance_uri}
|
||||
|
||||
val endOfPaginationReached = apiResponse.isEmpty()
|
||||
|
||||
db.withTransaction {
|
||||
// Clear table in the database
|
||||
if (loadType == LoadType.REFRESH) {
|
||||
db.directMessagesDao().clearFeedContent(user.user_id, user.instance_uri)
|
||||
}
|
||||
db.directMessagesDao().insertAll(apiResponse)
|
||||
}
|
||||
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
|
||||
} catch (exception: Exception){
|
||||
return MediatorResult.Error(exception)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
package org.pixeldroid.app.utils.api.objects
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import java.io.Serializable
|
||||
|
||||
/*
|
||||
@ -7,10 +11,31 @@ Represents a conversation.
|
||||
https://docs.joinmastodon.org/entities/Conversation/
|
||||
*/
|
||||
|
||||
@Entity(
|
||||
tableName = "direct_messages",
|
||||
primaryKeys = ["id", "user_id", "instance_uri"],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = UserDatabaseEntity::class,
|
||||
parentColumns = arrayOf("user_id", "instance_uri"),
|
||||
childColumns = arrayOf("user_id", "instance_uri"),
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
indices = [Index(value = ["user_id", "instance_uri"])]
|
||||
)
|
||||
data class Conversation(
|
||||
//Base attributes
|
||||
override val id: String?,
|
||||
val unread: Boolean? = true,
|
||||
val accounts: List<Account>? = null,
|
||||
val last_statuses: Status? = null
|
||||
) : Serializable, FeedContent
|
||||
val last_status: Status? = null,
|
||||
|
||||
//Database values (not from API)
|
||||
//TODO do we find this approach acceptable? Preferable to a semi-duplicate NotificationDataBaseEntity?
|
||||
override var user_id: String,
|
||||
override var instance_uri: String,
|
||||
): FeedContent, FeedContentDatabase, Serializable {
|
||||
enum class NotificationType : Serializable {
|
||||
follow, follow_request, mention, reblog, favourite, poll, status, comment //comment is Pixelfed-specific?
|
||||
}
|
||||
}
|
@ -14,13 +14,16 @@ import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.DirectMessagesDao
|
||||
|
||||
@Database(entities = [
|
||||
InstanceDatabaseEntity::class,
|
||||
UserDatabaseEntity::class,
|
||||
HomeStatusDatabaseEntity::class,
|
||||
PublicFeedStatusDatabaseEntity::class,
|
||||
Notification::class
|
||||
Notification::class,
|
||||
Conversation::class
|
||||
],
|
||||
version = 5
|
||||
)
|
||||
@ -31,6 +34,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun homePostDao(): HomePostDao
|
||||
abstract fun publicPostDao(): PublicPostDao
|
||||
abstract fun notificationDao(): NotificationDao
|
||||
abstract fun directMessagesDao(): DirectMessagesDao
|
||||
}
|
||||
|
||||
val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||
|
@ -0,0 +1,19 @@
|
||||
package org.pixeldroid.app.utils.db.dao.feedContent
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import org.pixeldroid.app.utils.api.objects.Conversation
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
|
||||
|
||||
@Dao
|
||||
interface DirectMessagesDao: FeedContentDao<Conversation> {
|
||||
|
||||
@Query("DELETE FROM direct_messages WHERE user_id=:userId AND instance_uri=:instanceUri")
|
||||
override suspend fun clearFeedContent(userId: String, instanceUri: String)
|
||||
|
||||
// TODO: might have to order by date or some other value
|
||||
@Query("""SELECT * FROM direct_messages WHERE user_id=:userId AND instance_uri=:instanceUri """)
|
||||
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, Conversation>
|
||||
}
|
@ -13,7 +13,4 @@ interface FeedContentDao<T: FeedContentDatabase>{
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAll(feedContent: List<T>)
|
||||
|
||||
suspend fun delete(id: String, userId: String, instanceUri: String)
|
||||
|
||||
}
|
@ -18,7 +18,4 @@ interface NotificationDao: FeedContentDao<Notification> {
|
||||
@Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri
|
||||
ORDER BY datetime(created_at) DESC LIMIT 1""")
|
||||
fun latestNotification(userId: String, instanceUri: String): Notification?
|
||||
|
||||
@Query("DELETE FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id")
|
||||
override suspend fun delete(id: String, userId: String, instanceUri: String)
|
||||
}
|
@ -16,7 +16,7 @@ interface HomePostDao: FeedContentDao<HomeStatusDatabaseEntity> {
|
||||
override suspend fun clearFeedContent(userId: String, instanceUri: String)
|
||||
|
||||
@Query("DELETE FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri AND id=:id")
|
||||
override suspend fun delete(id: String, userId: String, instanceUri: String)
|
||||
suspend fun delete(id: String, userId: String, instanceUri: String)
|
||||
|
||||
@Query("UPDATE homePosts SET bookmarked=:bookmarked WHERE user_id=:id AND instance_uri=:instanceUri AND id=:statusId")
|
||||
fun bookmarkStatus(id: String, instanceUri: String, statusId: String, bookmarked: Boolean)
|
||||
|
79
app/src/main/res/layout/fragment_conversations.xml
Normal file
79
app/src/main/res/layout/fragment_conversations.xml
Normal file
@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_margin="5dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/notification_type"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="July 23" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_type"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="6dp"
|
||||
android:paddingStart="38dp"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textStyle="bold"
|
||||
app:drawableStartCompat="@drawable/ic_heart"
|
||||
app:layout_constraintEnd_toStartOf="@+id/notification_time"
|
||||
app:layout_constraintStart_toStartOf="@+id/notification_avatar"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="RtlSymmetry"
|
||||
tools:text="fdsqfdsfsqdfdsfqdsfsdfsfddsfqsdsdfsqdf liked your post" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notification_avatar"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/notification_type"
|
||||
tools:src="@drawable/ic_default_user"
|
||||
android:contentDescription="@string/profile_picture" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notification_photo_thumbnail"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="14dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/notification_type"
|
||||
tools:src="@drawable/ic_default_user"
|
||||
tools:srcCompat="@tools:sample/backgrounds/scenic"
|
||||
android:contentDescription="@string/notification_thumbnail" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_post_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/notification_photo_thumbnail"
|
||||
app:layout_constraintHorizontal_bias="0.164"
|
||||
app:layout_constraintStart_toEndOf="@+id/notification_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@+id/notification_type"
|
||||
app:layout_constraintVertical_bias="0.408"
|
||||
tools:text="Post description" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
Loading…
x
Reference in New Issue
Block a user