add content to NotificationsFragment

This commit is contained in:
Konrad Pozniak 2020-09-14 17:58:02 +02:00
parent 1c7a82b472
commit c1a9897c2a
22 changed files with 598 additions and 37 deletions

View File

@ -78,9 +78,8 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.preference:preference:1.1.1")
implementation("androidx.emoji:emoji-bundled:1.1.0")
implementation("androidx.paging:paging-runtime-ktx:3.0.0-alpha05")
implementation("androidx.paging:paging-runtime-ktx:3.0.0-alpha06")
implementation("androidx.viewpager2:viewpager2:1.0.0")
implementation("androidx.window:window:1.0.0-alpha01")
implementation("androidx.room:room-ktx:$roomVersion")
kapt("androidx.room:room-compiler:$roomVersion")

View File

@ -25,7 +25,6 @@ import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars

View File

@ -23,7 +23,6 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.activity.viewModels
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

View File

@ -0,0 +1,155 @@
/*
* 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.notifications
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.text.parseAsHtml
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import at.connyduck.pixelcat.R
import at.connyduck.pixelcat.components.util.BindingHolder
import at.connyduck.pixelcat.components.util.extension.hide
import at.connyduck.pixelcat.databinding.ItemNotificationBinding
import at.connyduck.pixelcat.databinding.ItemNotificationFollowBinding
import at.connyduck.pixelcat.databinding.ItemReplyBinding
import at.connyduck.pixelcat.db.entitity.NotificationEntity
import at.connyduck.pixelcat.db.entitity.StatusEntity
import at.connyduck.pixelcat.db.entitity.TimelineAccountEntity
import at.connyduck.pixelcat.model.Notification
import coil.load
import coil.transform.RoundedCornersTransformation
import java.text.DateFormat
import java.text.SimpleDateFormat
interface NotificationActionListener {
fun onDetailsOpened(status: StatusEntity)
fun onProfileOpened(account: TimelineAccountEntity)
}
object NotificationDiffUtil : DiffUtil.ItemCallback<NotificationEntity>() {
override fun areItemsTheSame(oldItem: NotificationEntity, newItem: NotificationEntity): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: NotificationEntity, newItem: NotificationEntity): Boolean {
return oldItem == newItem
}
}
class NotificationAdapter(
private val listener: NotificationActionListener
) : PagingDataAdapter<NotificationEntity, BindingHolder<*>>(NotificationDiffUtil) {
private val dateTimeFormatter = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<*> {
val binding = when (viewType) {
MENTION -> ItemReplyBinding.inflate(LayoutInflater.from(parent.context), parent, false)
FOLLOW -> ItemNotificationFollowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
FAVOURITE -> ItemNotificationBinding.inflate(LayoutInflater.from(parent.context), parent, false)
REBLOG -> ItemNotificationBinding.inflate(LayoutInflater.from(parent.context), parent, false)
else -> throw IllegalStateException()
}
return BindingHolder(binding)
}
override fun onBindViewHolder(holder: BindingHolder<*>, position: Int) {
getItem(position)?.let { notification ->
when (holder.binding) {
is ItemReplyBinding -> holder.binding.bind(notification, dateTimeFormatter)
is ItemNotificationFollowBinding -> holder.binding.bind(notification, listener)
is ItemNotificationBinding -> holder.binding.bind(notification, listener)
}
}
}
override fun getItemViewType(position: Int): Int {
Log.d("NotificationAdapter", "$position ${getItem(position)}")
return when (getItem(position)?.type) {
Notification.Type.MENTION -> MENTION
Notification.Type.REBLOG -> REBLOG
Notification.Type.FAVOURITE -> FAVOURITE
Notification.Type.FOLLOW -> FOLLOW
else -> throw IllegalStateException()
}
}
companion object {
private const val MENTION = 1
private const val FOLLOW = 2
private const val FAVOURITE = 3
private const val REBLOG = 4
}
}
private fun ItemReplyBinding.bind(notification: NotificationEntity, dateTimeFormatter: DateFormat) {
val status = notification.status!!
postAvatar.load(status.account.avatar) {
transformations(RoundedCornersTransformation(25f))
}
postDisplayName.text = status.account.displayName
postName.text = "@${status.account.username}"
postDescription.text = status.content.parseAsHtml().trim()
postDate.text = dateTimeFormatter.format(status.createdAt)
postLikeButton.hide()
postReplyButton.hide()
}
private fun ItemNotificationFollowBinding.bind(notification: NotificationEntity, listener: NotificationActionListener) {
val account = notification.account
notificationText.text = notificationText.context.getString(R.string.notification_followed, notification.account.username)
notificationAvatar.load(account.avatar) {
transformations(RoundedCornersTransformation(25f))
}
notificationDisplayName.text = account.displayName
notificationName.text = account.username
root.setOnClickListener {
listener.onProfileOpened(account)
}
}
private fun ItemNotificationBinding.bind(notification: NotificationEntity, listener: NotificationActionListener) {
notificationAvatar.load(notification.account.avatar) {
transformations(RoundedCornersTransformation(25f))
}
if (notification.type == Notification.Type.REBLOG) {
notificationText.text = notificationText.context.getString(R.string.notification_reblogged, notification.account.username)
} else { // Notification.Type.FAVOURITE
notificationText.text = notificationText.context.getString(R.string.notification_favourited, notification.account.username)
}
root.setOnClickListener {
listener.onDetailsOpened(notification.status!!)
}
}

View File

@ -19,18 +19,77 @@
package at.connyduck.pixelcat.components.notifications
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadState
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.SimpleItemAnimator
import at.connyduck.pixelcat.R
import at.connyduck.pixelcat.components.profile.ProfileActivity
import at.connyduck.pixelcat.components.timeline.detail.DetailActivity
import at.connyduck.pixelcat.components.util.getColorForAttr
import at.connyduck.pixelcat.dagger.ViewModelFactory
import at.connyduck.pixelcat.databinding.FragmentNotificationsBinding
import at.connyduck.pixelcat.db.entitity.StatusEntity
import at.connyduck.pixelcat.db.entitity.TimelineAccountEntity
import at.connyduck.pixelcat.util.viewBinding
import dagger.android.support.DaggerFragment
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
class NotificationsFragment : DaggerFragment(R.layout.fragment_notifications) {
class NotificationsFragment :
DaggerFragment(R.layout.fragment_notifications),
NotificationActionListener {
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val notificationsViewModel: NotificationsViewModel by viewModels { viewModelFactory }
private val viewModel: NotificationsViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(FragmentNotificationsBinding::bind)
@ExperimentalPagingApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.notificationSwipeRefresh.setColorSchemeColors(
view.context.getColorForAttr(R.attr.pixelcat_gradient_color_start),
view.context.getColorForAttr(R.attr.pixelcat_gradient_color_end)
)
val adapter = NotificationAdapter(this)
binding.notificationRecyclerView.adapter = adapter
(binding.notificationRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
binding.notificationRecyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
lifecycleScope.launch {
viewModel.notificationsFlow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
binding.notificationSwipeRefresh.setOnRefreshListener {
adapter.refresh()
}
adapter.addLoadStateListener {
if (it.refresh != LoadState.Loading) {
binding.notificationSwipeRefresh.isRefreshing = false
}
}
}
override fun onDetailsOpened(status: StatusEntity) {
startActivity(DetailActivity.newIntent(requireContext(), status.id))
}
override fun onProfileOpened(account: TimelineAccountEntity) {
startActivity(ProfileActivity.newIntent(requireContext(), account.id))
}
companion object {
fun newInstance() = NotificationsFragment()

View File

@ -0,0 +1,73 @@
/*
* 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.notifications
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import at.connyduck.pixelcat.db.AppDatabase
import at.connyduck.pixelcat.db.entitity.NotificationEntity
import at.connyduck.pixelcat.db.entitity.toEntity
import at.connyduck.pixelcat.network.FediverseApi
@ExperimentalPagingApi
class NotificationsRemoteMediator(
private val accountId: Long,
private val api: FediverseApi,
private val db: AppDatabase
) : RemoteMediator<Int, NotificationEntity>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, NotificationEntity>
): MediatorResult {
val apiCall = when (loadType) {
LoadType.REFRESH -> {
api.notifications(limit = state.config.initialLoadSize, excludes = setOf("poll", "follow_request"))
}
LoadType.PREPEND -> {
return MediatorResult.Success(true)
}
LoadType.APPEND -> {
val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.id
api.notifications(maxId = maxId, limit = state.config.pageSize, excludes = setOf("poll", "follow_request"))
}
}
return apiCall.fold(
{ notificationResult ->
db.withTransaction {
if (loadType == LoadType.REFRESH) {
db.notificationsDao().clearAll(accountId)
}
db.notificationsDao().insertOrReplace(notificationResult.map { it.toEntity(accountId) })
}
MediatorResult.Success(endOfPaginationReached = notificationResult.isEmpty())
},
{
MediatorResult.Error(it)
}
)
}
override suspend fun initialize() = InitializeAction.SKIP_INITIAL_REFRESH
}

View File

@ -20,6 +20,35 @@
package at.connyduck.pixelcat.components.notifications
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import at.connyduck.pixelcat.db.AccountManager
import at.connyduck.pixelcat.db.AppDatabase
import at.connyduck.pixelcat.network.FediverseApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flatMapConcat
import javax.inject.Inject
class NotificationsViewModel @Inject constructor() : ViewModel()
class NotificationsViewModel @Inject constructor(
accountManager: AccountManager,
private val db: AppDatabase,
private val fediverseApi: FediverseApi
) : ViewModel() {
@OptIn(FlowPreview::class)
@ExperimentalPagingApi
val notificationsFlow = accountManager::activeAccount.asFlow()
.flatMapConcat { activeAccount ->
Pager(
config = PagingConfig(pageSize = 10, enablePlaceholders = false),
remoteMediator = NotificationsRemoteMediator(activeAccount?.id!!, fediverseApi, db),
pagingSourceFactory = { db.notificationsDao().notifications(activeAccount.id) }
).flow
}
.cachedIn(viewModelScope)
}

View File

@ -22,7 +22,6 @@ package at.connyduck.pixelcat.components.profile
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars

View File

@ -21,12 +21,14 @@ package at.connyduck.pixelcat.dagger
import at.connyduck.pixelcat.BuildConfig
import at.connyduck.pixelcat.db.AccountManager
import at.connyduck.pixelcat.model.Notification
import at.connyduck.pixelcat.network.FediverseApi
import at.connyduck.pixelcat.network.InstanceSwitchAuthInterceptor
import at.connyduck.pixelcat.network.RefreshTokenAuthenticator
import at.connyduck.pixelcat.network.UserAgentInterceptor
import at.connyduck.pixelcat.network.calladapter.NetworkResponseAdapterFactory
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.EnumJsonAdapter
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
import dagger.Module
import dagger.Provides
@ -69,6 +71,7 @@ class NetworkModule {
fun providesMoshi(): Moshi {
return Moshi.Builder()
.add(Date::class.java, Rfc3339DateJsonAdapter())
.add(Notification.Type::class.java, EnumJsonAdapter.create(Notification.Type::class.java).withUnknownFallback(Notification.Type.UNKNOWN))
.build()
}

View File

@ -22,12 +22,15 @@ package at.connyduck.pixelcat.db
import androidx.room.Database
import androidx.room.RoomDatabase
import at.connyduck.pixelcat.db.entitity.AccountEntity
import at.connyduck.pixelcat.db.entitity.NotificationEntity
import at.connyduck.pixelcat.db.entitity.StatusEntity
@Database(entities = [AccountEntity::class, StatusEntity::class], version = 1)
@Database(entities = [AccountEntity::class, StatusEntity::class, NotificationEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun accountDao(): AccountDao
abstract fun statusDao(): TimelineDao
abstract fun notificationsDao(): NotificationsDao
}

View File

@ -21,6 +21,7 @@ package at.connyduck.pixelcat.db
import androidx.room.TypeConverter
import at.connyduck.pixelcat.model.Attachment
import at.connyduck.pixelcat.model.Notification
import at.connyduck.pixelcat.model.Status
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
@ -31,14 +32,10 @@ class Converters {
private val moshi = Moshi.Builder().build()
@TypeConverter
fun visibilityToInt(visibility: Status.Visibility): String {
return visibility.name
}
fun visibilityToString(visibility: Status.Visibility) = visibility.name
@TypeConverter
fun stringToVisibility(visibility: String): Status.Visibility {
return Status.Visibility.valueOf(visibility)
}
fun stringToVisibility(visibility: String) = Status.Visibility.valueOf(visibility)
@TypeConverter
fun attachmentListToJson(attachmentList: List<Attachment>?): String {
@ -62,12 +59,14 @@ class Converters {
}
@TypeConverter
fun dateToLong(date: Date): Long {
return date.time
}
fun dateToLong(date: Date) = date.time
@TypeConverter
fun longToDate(date: Long): Date {
return Date(date)
}
fun longToDate(date: Long) = Date(date)
@TypeConverter
fun notificationTypeToString(type: Notification.Type) = type.name
@TypeConverter
fun stringToNotificationType(type: String) = Notification.Type.valueOf(type)
}

View File

@ -0,0 +1,47 @@
/*
* 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.db
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import at.connyduck.pixelcat.db.entitity.NotificationEntity
@Dao
interface NotificationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrReplace(notification: NotificationEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrReplace(statuses: List<NotificationEntity>)
@Delete
suspend fun delete(status: NotificationEntity)
@Query("SELECT * FROM NotificationEntity WHERE accountId = :accountId ORDER BY LENGTH(id) DESC, id DESC")
fun notifications(accountId: Long): PagingSource<Int, NotificationEntity>
@Query("DELETE FROM NotificationEntity WHERE accountId = :accountId")
suspend fun clearAll(accountId: Long)
}

View File

@ -0,0 +1,44 @@
/*
* 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.db.entitity
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.TypeConverters
import at.connyduck.pixelcat.db.Converters
import at.connyduck.pixelcat.model.Notification
@Entity(primaryKeys = ["accountId", "id"])
@TypeConverters(Converters::class)
data class NotificationEntity(
val accountId: Long,
val type: Notification.Type,
val id: String,
@Embedded(prefix = "a_") val account: TimelineAccountEntity,
@Embedded(prefix = "s_") val status: StatusEntity?
)
fun Notification.toEntity(accountId: Long) = NotificationEntity(
accountId = accountId,
type = type,
id = id,
account = account.toEntity(accountId),
status = status?.toEntity(accountId, 0, false)
)

View File

@ -19,14 +19,10 @@
package at.connyduck.pixelcat.db.entitity
import androidx.room.Entity
import at.connyduck.pixelcat.model.Account
@Entity(
primaryKeys = ["serverId", "timelineUserId"]
)
data class TimelineAccountEntity(
val serverId: Long,
val accountId: Long,
val id: String,
val localUsername: String,
val username: String,
@ -35,8 +31,8 @@ data class TimelineAccountEntity(
val avatar: String
)
fun Account.toEntity(serverId: Long) = TimelineAccountEntity(
serverId = serverId,
fun Account.toEntity(accountId: Long) = TimelineAccountEntity(
accountId = accountId,
id = id,
localUsername = localUsername,
username = username,

View File

@ -0,0 +1,49 @@
/*
* 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.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Notification(
val type: Type,
val id: String,
val account: Account,
val status: Status?
) {
@JsonClass(generateAdapter = false)
enum class Type {
UNKNOWN,
@Json(name = "mention")
MENTION,
@Json(name = "reblog")
REBLOG,
@Json(name = "favourite")
FAVOURITE,
@Json(name = "follow")
FOLLOW,
@Json(name = "follow_request")
FOLLOW_REQUEST,
@Json(name = "poll")
POLL
}
}

View File

@ -24,6 +24,7 @@ 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.Notification
import at.connyduck.pixelcat.model.Relationship
import at.connyduck.pixelcat.model.Status
import at.connyduck.pixelcat.model.StatusContext
@ -40,6 +41,7 @@ import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
@JvmSuppressWildcards
interface FediverseApi {
companion object {
@ -199,4 +201,12 @@ interface FediverseApi {
suspend fun statusContext(
@Path("id") statusId: String
): NetworkResponse<StatusContext>
@GET("api/v1/notifications")
suspend fun notifications(
@Query("max_id") maxId: String? = null,
@Query("since_id") sinceId: String? = null,
@Query("limit") limit: Int? = null,
@Query("exclude_types[]") excludes: Set<String>? = null
): NetworkResponse<List<Notification>>
}

View File

@ -15,23 +15,24 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_cat_small"
app:title="@string/app_name"
app:title="@string/title_notifications"
app:titleTextAppearance="@style/TextAppearanceToolbar"
app:titleTextColor="?attr/colorPrimary" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/timelineSwipeRefresh"
android:id="@+id/notificationSwipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/timelineRecyclerView"
android:id="@+id/notificationRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout>

View File

@ -0,0 +1,34 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/notificationAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="#f00" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/notificationText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:maxLines="2"
app:layout_constraintBottom_toBottomOf="@id/notificationAvatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/notificationAvatar"
app:layout_constraintTop_toTopOf="@id/notificationAvatar"
tools:text="Conny Duck liked your post!" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,58 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.emoji.widget.EmojiTextView
android:id="@+id/notificationText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent"
tools:text="\@ConnyDuck followed you" />
<ImageView
android:id="@+id/notificationAvatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/notificationText"
tools:background="#f00" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/notificationDisplayName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:lines="1"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/notificationName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/notificationAvatar"
app:layout_constraintTop_toTopOf="@id/notificationAvatar"
tools:text="Conny Duck" />
<TextView
android:id="@+id/notificationName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:lines="1"
app:layout_constraintBottom_toBottomOf="@id/notificationAvatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/notificationAvatar"
app:layout_constraintTop_toBottomOf="@id/notificationDisplayName"
tools:text="\@connyduck\@chaos.social" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,12 +9,12 @@
<item
android:id="@+id/navigation_search"
android:icon="@drawable/ic_search"
android:title="@string/title_dashboard"/>
android:title="@string/title_search"/>
<item
android:id="@+id/navigation_compose"
android:icon="@drawable/ic_plus_square"
android:title="@string/title_notifications"/>
android:title="@string/title_compose"/>
<item
android:id="@+id/navigation_notifications"
android:icon="@drawable/ic_heart"
@ -22,6 +22,6 @@
<item
android:id="@+id/navigation_profile"
android:icon="@drawable/ic_user"
android:title="@string/title_notifications"/>
android:title="@string/title_profile"/>
</menu>

View File

@ -1,8 +1,10 @@
<resources>
<string name="app_name">Pixelcat</string>
<string name="title_home">Home</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_search">Search</string>
<string name="title_compose">Compose new status</string>
<string name="title_notifications">Notifications</string>
<string name="title_profile">Profile</string>
<string name="login_button">Login</string>
<string name="instance_input_hint">Which instance?</string>
@ -79,7 +81,10 @@
<string name="status_general_error">An unexpected error occurred</string>
<string name="status_network_error">Failed to connect. Please check your internet connection-</string>
<string name="status_details_replying_to">Replying to $1%s</string>
<string name="status_details_replying_to">Replying to %1$s</string>
<string name="notification_reblogged">%1$s boosted your post</string>
<string name="notification_favourited">%1$s liked your post</string>
<string name="notification_followed">%1$s followed you</string>
</resources>