add content to NotificationsFragment
This commit is contained in:
parent
1c7a82b472
commit
c1a9897c2a
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!!)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
)
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>>
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue