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.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.preference:preference:1.1.1") implementation("androidx.preference:preference:1.1.1")
implementation("androidx.emoji:emoji-bundled:1.1.0") 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.viewpager2:viewpager2:1.0.0")
implementation("androidx.window:window:1.0.0-alpha01")
implementation("androidx.room:room-ktx:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion")
kapt("androidx.room:room-compiler:$roomVersion") kapt("androidx.room:room-compiler:$roomVersion")

View File

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

View File

@ -23,7 +23,6 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat 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 package at.connyduck.pixelcat.components.notifications
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels 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.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.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 dagger.android.support.DaggerFragment
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class NotificationsFragment : DaggerFragment(R.layout.fragment_notifications) { class NotificationsFragment :
DaggerFragment(R.layout.fragment_notifications),
NotificationActionListener {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory 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 { companion object {
fun newInstance() = NotificationsFragment() 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 package at.connyduck.pixelcat.components.notifications
import androidx.lifecycle.ViewModel 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 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.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.ViewGroup
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars 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.BuildConfig
import at.connyduck.pixelcat.db.AccountManager import at.connyduck.pixelcat.db.AccountManager
import at.connyduck.pixelcat.model.Notification
import at.connyduck.pixelcat.network.FediverseApi import at.connyduck.pixelcat.network.FediverseApi
import at.connyduck.pixelcat.network.InstanceSwitchAuthInterceptor import at.connyduck.pixelcat.network.InstanceSwitchAuthInterceptor
import at.connyduck.pixelcat.network.RefreshTokenAuthenticator import at.connyduck.pixelcat.network.RefreshTokenAuthenticator
import at.connyduck.pixelcat.network.UserAgentInterceptor import at.connyduck.pixelcat.network.UserAgentInterceptor
import at.connyduck.pixelcat.network.calladapter.NetworkResponseAdapterFactory import at.connyduck.pixelcat.network.calladapter.NetworkResponseAdapterFactory
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.EnumJsonAdapter
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -69,6 +71,7 @@ class NetworkModule {
fun providesMoshi(): Moshi { fun providesMoshi(): Moshi {
return Moshi.Builder() return Moshi.Builder()
.add(Date::class.java, Rfc3339DateJsonAdapter()) .add(Date::class.java, Rfc3339DateJsonAdapter())
.add(Notification.Type::class.java, EnumJsonAdapter.create(Notification.Type::class.java).withUnknownFallback(Notification.Type.UNKNOWN))
.build() .build()
} }

View File

@ -22,12 +22,15 @@ package at.connyduck.pixelcat.db
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import at.connyduck.pixelcat.db.entitity.AccountEntity import at.connyduck.pixelcat.db.entitity.AccountEntity
import at.connyduck.pixelcat.db.entitity.NotificationEntity
import at.connyduck.pixelcat.db.entitity.StatusEntity 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 class AppDatabase : RoomDatabase() {
abstract fun accountDao(): AccountDao abstract fun accountDao(): AccountDao
abstract fun statusDao(): TimelineDao abstract fun statusDao(): TimelineDao
abstract fun notificationsDao(): NotificationsDao
} }

View File

@ -21,6 +21,7 @@ package at.connyduck.pixelcat.db
import androidx.room.TypeConverter import androidx.room.TypeConverter
import at.connyduck.pixelcat.model.Attachment import at.connyduck.pixelcat.model.Attachment
import at.connyduck.pixelcat.model.Notification
import at.connyduck.pixelcat.model.Status import at.connyduck.pixelcat.model.Status
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types import com.squareup.moshi.Types
@ -31,14 +32,10 @@ class Converters {
private val moshi = Moshi.Builder().build() private val moshi = Moshi.Builder().build()
@TypeConverter @TypeConverter
fun visibilityToInt(visibility: Status.Visibility): String { fun visibilityToString(visibility: Status.Visibility) = visibility.name
return visibility.name
}
@TypeConverter @TypeConverter
fun stringToVisibility(visibility: String): Status.Visibility { fun stringToVisibility(visibility: String) = Status.Visibility.valueOf(visibility)
return Status.Visibility.valueOf(visibility)
}
@TypeConverter @TypeConverter
fun attachmentListToJson(attachmentList: List<Attachment>?): String { fun attachmentListToJson(attachmentList: List<Attachment>?): String {
@ -62,12 +59,14 @@ class Converters {
} }
@TypeConverter @TypeConverter
fun dateToLong(date: Date): Long { fun dateToLong(date: Date) = date.time
return date.time
}
@TypeConverter @TypeConverter
fun longToDate(date: Long): Date { fun longToDate(date: Long) = Date(date)
return 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 package at.connyduck.pixelcat.db.entitity
import androidx.room.Entity
import at.connyduck.pixelcat.model.Account import at.connyduck.pixelcat.model.Account
@Entity(
primaryKeys = ["serverId", "timelineUserId"]
)
data class TimelineAccountEntity( data class TimelineAccountEntity(
val serverId: Long, val accountId: Long,
val id: String, val id: String,
val localUsername: String, val localUsername: String,
val username: String, val username: String,
@ -35,8 +31,8 @@ data class TimelineAccountEntity(
val avatar: String val avatar: String
) )
fun Account.toEntity(serverId: Long) = TimelineAccountEntity( fun Account.toEntity(accountId: Long) = TimelineAccountEntity(
serverId = serverId, accountId = accountId,
id = id, id = id,
localUsername = localUsername, localUsername = localUsername,
username = username, 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.AppCredentials
import at.connyduck.pixelcat.model.Attachment import at.connyduck.pixelcat.model.Attachment
import at.connyduck.pixelcat.model.NewStatus import at.connyduck.pixelcat.model.NewStatus
import at.connyduck.pixelcat.model.Notification
import at.connyduck.pixelcat.model.Relationship import at.connyduck.pixelcat.model.Relationship
import at.connyduck.pixelcat.model.Status import at.connyduck.pixelcat.model.Status
import at.connyduck.pixelcat.model.StatusContext import at.connyduck.pixelcat.model.StatusContext
@ -40,6 +41,7 @@ import retrofit2.http.Part
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
@JvmSuppressWildcards
interface FediverseApi { interface FediverseApi {
companion object { companion object {
@ -199,4 +201,12 @@ interface FediverseApi {
suspend fun statusContext( suspend fun statusContext(
@Path("id") statusId: String @Path("id") statusId: String
): NetworkResponse<StatusContext> ): 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_cat_small" app:navigationIcon="@drawable/ic_cat_small"
app:title="@string/app_name" app:title="@string/title_notifications"
app:titleTextAppearance="@style/TextAppearanceToolbar" app:titleTextAppearance="@style/TextAppearanceToolbar"
app:titleTextColor="?attr/colorPrimary" /> app:titleTextColor="?attr/colorPrimary" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/timelineSwipeRefresh" android:id="@+id/notificationSwipeRefresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/timelineRecyclerView" android:id="@+id/notificationRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scrollbars="vertical" android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout> </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 <item
android:id="@+id/navigation_search" android:id="@+id/navigation_search"
android:icon="@drawable/ic_search" android:icon="@drawable/ic_search"
android:title="@string/title_dashboard"/> android:title="@string/title_search"/>
<item <item
android:id="@+id/navigation_compose" android:id="@+id/navigation_compose"
android:icon="@drawable/ic_plus_square" android:icon="@drawable/ic_plus_square"
android:title="@string/title_notifications"/> android:title="@string/title_compose"/>
<item <item
android:id="@+id/navigation_notifications" android:id="@+id/navigation_notifications"
android:icon="@drawable/ic_heart" android:icon="@drawable/ic_heart"
@ -22,6 +22,6 @@
<item <item
android:id="@+id/navigation_profile" android:id="@+id/navigation_profile"
android:icon="@drawable/ic_user" android:icon="@drawable/ic_user"
android:title="@string/title_notifications"/> android:title="@string/title_profile"/>
</menu> </menu>

View File

@ -1,8 +1,10 @@
<resources> <resources>
<string name="app_name">Pixelcat</string> <string name="app_name">Pixelcat</string>
<string name="title_home">Home</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_notifications">Notifications</string>
<string name="title_profile">Profile</string>
<string name="login_button">Login</string> <string name="login_button">Login</string>
<string name="instance_input_hint">Which instance?</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_general_error">An unexpected error occurred</string>
<string name="status_network_error">Failed to connect. Please check your internet connection-</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> </resources>