Compare commits

...

3 Commits

Author SHA1 Message Date
MarieJ
7f3ab1e4a6 WIP 2024-01-18 18:32:47 +01:00
MarieJ
0fe2c54940 Merge master 2024-01-11 19:55:00 +01:00
MarieJ
86b0fe1c60 Started DMs 2024-01-11 19:32:50 +01:00
18 changed files with 634 additions and 17 deletions

View File

@ -23,9 +23,14 @@
android:name=".utils.PixelDroidApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/BaseAppTheme">
<activity
android:name=".directMessages.ConversationsActivity"
android:exported="false" />
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
@ -38,19 +43,27 @@
<activity
android:name=".posts.AlbumActivity"
android:exported="false"
android:theme="@style/AppTheme.ActionBar.Transparent"/>
android:theme="@style/AppTheme.ActionBar.Transparent" />
<activity
android:name=".profile.EditProfileActivity"
android:exported="false"
android:theme="@style/BaseAppTheme" />
<activity
android:name=".posts.MediaViewerActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="false"
android:theme="@style/BaseAppTheme" />
<activity android:name=".postCreation.camera.CameraActivity"
android:theme="@style/BaseAppTheme"/>
android:theme="@style/BaseAppTheme.NoActionBar" />
<activity android:name=".postCreation.camera.CameraActivity" />
<activity
android:name=".postCreation.camera.CameraActivityShortcut"
android:exported="true"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<activity
android:name=".posts.ReportActivity"
android:screenOrientation="sensorPortrait"
@ -105,8 +118,8 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.App.Starting"
android:screenOrientation="sensorPortrait"
android:theme="@style/Theme.App.Starting"
android:windowSoftInputMode="adjustPan"
tools:ignore="LockedOrientationActivity">
<intent-filter>
@ -118,7 +131,8 @@
<meta-data
android:name="android.app.default_searchable"
android:value="org.pixeldroid.app.searchDiscover.SearchActivity" />
<meta-data android:name="android.app.shortcuts"
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity

View File

@ -43,6 +43,7 @@ import com.mikepenz.materialdrawer.widget.AccountHeaderView
import kotlinx.coroutines.launch
import org.ligi.tracedroid.sending.sendTraceDroidStackTracesIfExist
import org.pixeldroid.app.databinding.ActivityMainBinding
import org.pixeldroid.app.directMessages.ConversationsActivity
import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.posts.NestedScrollableHost
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
@ -237,6 +238,7 @@ class MainActivity : BaseActivity() {
1 -> launchActivity(ProfileActivity())
2 -> launchActivity(SettingsActivity())
3 -> logOut()
4 -> launchActivity(ConversationsActivity())
}
false
}

View File

@ -0,0 +1,19 @@
package org.pixeldroid.app.directMessages
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import org.pixeldroid.app.R
import org.pixeldroid.app.directMessages.ui.main.ConversationsFragment
class ConversationsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_conversations)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, ConversationsFragment.newInstance())
.commitNow()
}
}
}

View File

@ -0,0 +1,32 @@
package org.pixeldroid.app.directMessages.ui.main
import androidx.lifecycle.ViewModelProvider
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.pixeldroid.app.R
class ConversationsFragment : Fragment() {
companion object {
fun newInstance() = ConversationsFragment()
}
private lateinit var viewModel: ConversationsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(ConversationsViewModel::class.java)
// TODO: Use the ViewModel to watch the variable containing DM data and show it
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_main, container, false)
}
}

View File

@ -0,0 +1,25 @@
package org.pixeldroid.app.directMessages.ui.main
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import javax.inject.Inject
class ConversationsViewModel(application: Application) : AndroidViewModel(application) {
// TODO: Implement the ViewModel
// API calls for DM, store results in some variable here
@Inject
lateinit var apiHolder: PixelfedAPIHolder
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
}
suspend fun loadConversations() {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val conversations = api.viewAllConversations()
}
}

View File

@ -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
)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -332,6 +332,24 @@ interface PixelfedAPI {
@Query("account_id") account_id: Boolean? = null
): List<Notification>
@GET("/api/v1/conversations")
suspend fun viewAllConversations(
@Query("max_id") max_id: String? = null,
@Query("since_id") since_id: String? = null,
@Query("min_id") min_id: String? = null,
@Query("limit") limit: String? = null
): List<Conversation>
@DELETE("/api/v1/conversations/{id}")
suspend fun deleteConversation(
@Path("id") id: String
)
@POST("/api/v1/conversations/{id}/read")
suspend fun markConversationAsRead(
@Path("id") id: String
): Conversation
@GET("/api/v1/accounts/verify_credentials")
suspend fun verifyCredentials(
//The authorization header needs to be of the form "Bearer <token>"

View File

@ -0,0 +1,41 @@
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
/*
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_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?
}
}

View File

@ -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) {

View File

@ -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>
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)

View File

@ -7,6 +7,7 @@ import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.BaseFragment
import dagger.Component
import org.pixeldroid.app.directMessages.ui.main.ConversationsViewModel
import org.pixeldroid.app.postCreation.PostCreationViewModel
import org.pixeldroid.app.profile.EditProfileViewModel
import org.pixeldroid.app.stories.StoriesViewModel
@ -24,6 +25,7 @@ interface ApplicationComponent {
fun inject(notificationsWorker: NotificationsWorker)
fun inject(postCreationViewModel: PostCreationViewModel)
fun inject(editProfileViewModel: EditProfileViewModel)
fun inject(editProfileViewModel: ConversationsViewModel)
fun inject(storiesViewModel: StoriesViewModel)
val context: Context?

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ConversationsActivity" />

View 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>

View File

@ -0,0 +1,20 @@
<?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:tools="http://schemas.android.com/tools"
android:id="@+id/conversations"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.ConversationsFragment">
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ConversationsFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>