From c1a9897c2a18b5c1aa54fdfe9fe752d8d37808b9 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 14 Sep 2020 17:58:02 +0200 Subject: [PATCH] add content to NotificationsFragment --- app/build.gradle.kts | 3 +- .../components/compose/ComposeActivity.kt | 1 - .../pixelcat/components/main/MainActivity.kt | 1 - .../notifications/NotificationAdapter.kt | 155 ++++++++++++++++++ .../notifications/NotificationsFragment.kt | 63 ++++++- .../NotificationsRemoteMediator.kt | 73 +++++++++ .../notifications/NotificationsViewModel.kt | 31 +++- .../components/profile/ProfileActivity.kt | 1 - .../pixelcat/dagger/NetworkModule.kt | 3 + .../at/connyduck/pixelcat/db/AppDatabase.kt | 5 +- .../at/connyduck/pixelcat/db/Converters.kt | 23 ++- .../connyduck/pixelcat/db/NotificationsDao.kt | 47 ++++++ .../db/entitity/NotificationEntity.kt | 44 +++++ ...imelineStatusEntity.kt => StatusEntity.kt} | 0 .../db/entitity/TimelineAccountEntity.kt | 10 +- .../connyduck/pixelcat/model/Notification.kt | 49 ++++++ .../pixelcat/network/FediverseApi.kt | 10 ++ .../res/layout/fragment_notifications.xml | 9 +- app/src/main/res/layout/item_notification.xml | 34 ++++ .../res/layout/item_notification_follow.xml | 58 +++++++ app/src/main/res/menu/navigation.xml | 6 +- app/src/main/res/values/strings.xml | 9 +- 22 files changed, 598 insertions(+), 37 deletions(-) create mode 100644 app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationAdapter.kt create mode 100644 app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsRemoteMediator.kt create mode 100644 app/src/main/kotlin/at/connyduck/pixelcat/db/NotificationsDao.kt create mode 100644 app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/NotificationEntity.kt rename app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/{TimelineStatusEntity.kt => StatusEntity.kt} (100%) create mode 100644 app/src/main/kotlin/at/connyduck/pixelcat/model/Notification.kt create mode 100644 app/src/main/res/layout/item_notification.xml create mode 100644 app/src/main/res/layout/item_notification_follow.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 928f767..a0f90b7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/compose/ComposeActivity.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/compose/ComposeActivity.kt index bd7180e..82f83d0 100644 --- a/app/src/main/kotlin/at/connyduck/pixelcat/components/compose/ComposeActivity.kt +++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/compose/ComposeActivity.kt @@ -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 diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/main/MainActivity.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/main/MainActivity.kt index bc911d3..0aba3af 100644 --- a/app/src/main/kotlin/at/connyduck/pixelcat/components/main/MainActivity.kt +++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/main/MainActivity.kt @@ -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 diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationAdapter.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationAdapter.kt new file mode 100644 index 0000000..ed2fb4a --- /dev/null +++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationAdapter.kt @@ -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 . + */ + +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() { + 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>(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!!) + } +} diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsFragment.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsFragment.kt index e3f7f6a..f66a1bb 100644 --- a/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsFragment.kt +++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsFragment.kt @@ -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() diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsRemoteMediator.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsRemoteMediator.kt new file mode 100644 index 0000000..39ac2f9 --- /dev/null +++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsRemoteMediator.kt @@ -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 . + */ + +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() { + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): 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 +} diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsViewModel.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsViewModel.kt index a33f5de..b0dbbef 100644 --- a/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsViewModel.kt +++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/notifications/NotificationsViewModel.kt @@ -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) + +} diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/components/profile/ProfileActivity.kt b/app/src/main/kotlin/at/connyduck/pixelcat/components/profile/ProfileActivity.kt index 57c0cf3..1762856 100644 --- a/app/src/main/kotlin/at/connyduck/pixelcat/components/profile/ProfileActivity.kt +++ b/app/src/main/kotlin/at/connyduck/pixelcat/components/profile/ProfileActivity.kt @@ -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 diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/dagger/NetworkModule.kt b/app/src/main/kotlin/at/connyduck/pixelcat/dagger/NetworkModule.kt index 3a12c3a..87300ad 100644 --- a/app/src/main/kotlin/at/connyduck/pixelcat/dagger/NetworkModule.kt +++ b/app/src/main/kotlin/at/connyduck/pixelcat/dagger/NetworkModule.kt @@ -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() } diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/db/AppDatabase.kt b/app/src/main/kotlin/at/connyduck/pixelcat/db/AppDatabase.kt index 5178b6e..933da09 100644 --- a/app/src/main/kotlin/at/connyduck/pixelcat/db/AppDatabase.kt +++ b/app/src/main/kotlin/at/connyduck/pixelcat/db/AppDatabase.kt @@ -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 } diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/db/Converters.kt b/app/src/main/kotlin/at/connyduck/pixelcat/db/Converters.kt index 3b8da9c..3a50c74 100644 --- a/app/src/main/kotlin/at/connyduck/pixelcat/db/Converters.kt +++ b/app/src/main/kotlin/at/connyduck/pixelcat/db/Converters.kt @@ -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?): 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) } diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/db/NotificationsDao.kt b/app/src/main/kotlin/at/connyduck/pixelcat/db/NotificationsDao.kt new file mode 100644 index 0000000..af7abe1 --- /dev/null +++ b/app/src/main/kotlin/at/connyduck/pixelcat/db/NotificationsDao.kt @@ -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 . + */ + +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) + + @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 + + @Query("DELETE FROM NotificationEntity WHERE accountId = :accountId") + suspend fun clearAll(accountId: Long) +} diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/NotificationEntity.kt b/app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/NotificationEntity.kt new file mode 100644 index 0000000..8bc91b6 --- /dev/null +++ b/app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/NotificationEntity.kt @@ -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 . + */ + +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) +) diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/TimelineStatusEntity.kt b/app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/StatusEntity.kt similarity index 100% rename from app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/TimelineStatusEntity.kt rename to app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/StatusEntity.kt diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/TimelineAccountEntity.kt b/app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/TimelineAccountEntity.kt index 02973f5..a4b410b 100644 --- a/app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/TimelineAccountEntity.kt +++ b/app/src/main/kotlin/at/connyduck/pixelcat/db/entitity/TimelineAccountEntity.kt @@ -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, diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/model/Notification.kt b/app/src/main/kotlin/at/connyduck/pixelcat/model/Notification.kt new file mode 100644 index 0000000..c2112aa --- /dev/null +++ b/app/src/main/kotlin/at/connyduck/pixelcat/model/Notification.kt @@ -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 . + */ + +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 + } +} diff --git a/app/src/main/kotlin/at/connyduck/pixelcat/network/FediverseApi.kt b/app/src/main/kotlin/at/connyduck/pixelcat/network/FediverseApi.kt index a7c64ba..5f63721 100644 --- a/app/src/main/kotlin/at/connyduck/pixelcat/network/FediverseApi.kt +++ b/app/src/main/kotlin/at/connyduck/pixelcat/network/FediverseApi.kt @@ -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 + + @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? = null + ): NetworkResponse> } diff --git a/app/src/main/res/layout/fragment_notifications.xml b/app/src/main/res/layout/fragment_notifications.xml index 38d5870..14f7a0f 100644 --- a/app/src/main/res/layout/fragment_notifications.xml +++ b/app/src/main/res/layout/fragment_notifications.xml @@ -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" /> - + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notification.xml b/app/src/main/res/layout/item_notification.xml new file mode 100644 index 0000000..781ea4d --- /dev/null +++ b/app/src/main/res/layout/item_notification.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notification_follow.xml b/app/src/main/res/layout/item_notification_follow.xml new file mode 100644 index 0000000..b90cfba --- /dev/null +++ b/app/src/main/res/layout/item_notification_follow.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/navigation.xml b/app/src/main/res/menu/navigation.xml index b3b09bf..c886cf8 100644 --- a/app/src/main/res/menu/navigation.xml +++ b/app/src/main/res/menu/navigation.xml @@ -9,12 +9,12 @@ + android:title="@string/title_search"/> + android:title="@string/title_compose"/> + android:title="@string/title_profile"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0a63e06..b1c6d05 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,8 +1,10 @@ Pixelcat Home - Dashboard + Search + Compose new status Notifications + Profile Login Which instance? @@ -79,7 +81,10 @@ An unexpected error occurred Failed to connect. Please check your internet connection- - Replying to $1%s + Replying to %1$s + %1$s boosted your post + %1$s liked your post + %1$s followed you