feat: notifications page rework

This commit is contained in:
aayush262
2024-05-26 00:40:46 +05:30
parent 37949c7e8e
commit 2b4c9bf7a9
11 changed files with 487 additions and 324 deletions

View File

@@ -0,0 +1,94 @@
package ani.dantotsu.profile.notification
import android.os.Bundle
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.databinding.ActivityNotificationBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import nl.joery.animatedbottombar.AnimatedBottomBar
class NotificationActivity : AppCompatActivity() {
private lateinit var binding: ActivityNotificationBinding
private var selected: Int = 0
lateinit var navBar: AnimatedBottomBar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
binding = ActivityNotificationBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.notificationTitle.text = getString(R.string.notifications)
binding.notificationToolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
navBar = binding.notificationNavBar
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
val mediaTab = navBar.createTab(R.drawable.ic_round_movie_filter_24, "Media")
val userTab = navBar.createTab(R.drawable.ic_round_person_24, "User")
val subscriptionTab = navBar.createTab(R.drawable.ic_round_notifications_active_24, "Subscriptions")
val commentTab = navBar.createTab(R.drawable.ic_round_comment_24, "Comments")
navBar.addTab(mediaTab)
navBar.addTab(userTab)
navBar.addTab(subscriptionTab)
navBar.addTab(commentTab)
binding.notificationBack.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
val getOne = intent.getIntExtra("activityId", -1)
binding.notificationViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, getOne)
binding.notificationViewPager.setOffscreenPageLimit(4)
binding.notificationViewPager.setCurrentItem(selected, false)
binding.notificationViewPager
navBar.selectTabAt(selected)
navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
selected = newIndex
binding.notificationViewPager.setCurrentItem(selected, true)
}
})
binding.notificationViewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
navBar.selectTabAt(position)
}
})
}
override fun onResume() {
if (this::navBar.isInitialized) {
navBar.selectTabAt(selected)
}
super.onResume()
}
private class ViewPagerAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle,
val id: Int = -1
) : FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int = 4
override fun createFragment(position: Int): Fragment = when (position) {
0 -> NotificationFragment(if (id != -1) "getOne" else "media", id)
1 -> NotificationFragment("user")
2 -> NotificationFragment("subscription")
3 -> NotificationFragment("comment")
else -> NotificationFragment("media")
}
}
}

View File

@@ -0,0 +1,238 @@
package ani.dantotsu.profile.notification
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Notification
import ani.dantotsu.databinding.FragmentNotificationsBinding
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.comment.CommentStore
import ani.dantotsu.notifications.subscription.SubscriptionStore
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity
import ani.dantotsu.setBaseline
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.xwray.groupie.GroupieAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class NotificationFragment(val type: String, private val getID: Int = -1) : Fragment() {
private lateinit var binding: FragmentNotificationsBinding
private var adapter: GroupieAdapter = GroupieAdapter()
private var currentPage = 1
private var hasNextPage = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentNotificationsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navbar = (activity as NotificationActivity).navBar
binding.listRecyclerView.setBaseline(navbar)
binding.listRecyclerView.adapter = adapter
binding.listRecyclerView.layoutManager = LinearLayoutManager(context)
binding.emptyTextView.text = getString(R.string.no_notifications)
lifecycleScope.launch {
val list = when (type) {
"getOne" -> getOne(getID)
"media" -> getMediaUpdates()
"user" -> getUserUpdates()
"subscription" -> getSubscriptions()
"comment" -> getComments()
else -> listOf()
}
adapter.addAll(list.map { NotificationItem(it, ::onClick) })
if (adapter.itemCount != 0) {
binding.listProgressBar.isVisible = false
}else{
binding.listProgressBar.isVisible = false
binding.emptyTextView.isVisible = true
}
}
binding.followSwipeRefresh.setOnRefreshListener {
lifecycleScope.launch {
adapter.clear()
currentPage = 1
val list = when (type) {
"getOne" -> getOne(getID)
"media" -> getMediaUpdates()
"user" -> getUserUpdates()
"subscription" -> getSubscriptions()
"comment" -> getComments()
else -> listOf()
}
adapter.addAll(list.map { NotificationItem(it, ::onClick) })
binding.followSwipeRefresh.isRefreshing = false
}
}
binding.listRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (shouldLoadMore()) {
lifecycleScope.launch {
val list = when (type) {
"media" -> getMediaUpdates()
"user" -> getUserUpdates()
else -> listOf()
}
binding.followRefresh.visibility = View.VISIBLE
adapter.addAll(list.map {
NotificationItem(
it,
::onClick
)
})
binding.followRefresh.visibility = View.GONE
}
}
}
})
}
private suspend fun getNotificationsFiltered(filter: (Notification) -> Boolean): List<Notification> {
val userId = Anilist.userid ?: PrefManager.getVal<String>(PrefName.AnilistUserId).toIntOrNull() ?: 0
val res = withContext(Dispatchers.IO) {
Anilist.query.getNotifications(userId, currentPage)
}
currentPage = res?.data?.page?.pageInfo?.currentPage?.plus(1) ?: 1
hasNextPage = res?.data?.page?.pageInfo?.hasNextPage ?: false
return res?.data?.page?.notifications?.filter(filter) ?: listOf()
}
private suspend fun getMediaUpdates(): List<Notification> {
return getNotificationsFiltered { it.media != null }
}
private suspend fun getUserUpdates(): List<Notification> {
return getNotificationsFiltered { it.media == null }
}
private suspend fun getOne(id: Int): List<Notification> {
return getNotificationsFiltered { it.id == id }
}
private fun getSubscriptions(): List<Notification> {
val list = PrefManager.getNullableVal<List<SubscriptionStore>>(
PrefName.SubscriptionNotificationStore,
null
) ?: listOf()
return list.sortedByDescending{ (it.time / 1000L).toInt() }
.filter{ it.image != null }.map {
Notification(
it.type,
System.currentTimeMillis().toInt(),
commentId = it.mediaId,
mediaId = it.mediaId,
notificationType = it.type,
context = it.title + ": " + it.content,
createdAt = (it.time / 1000L).toInt(),
image = it.image,
banner = it.banner ?: it.image
)
}
}
private fun getComments(): List<Notification> {
val list = PrefManager.getNullableVal<List<CommentStore>>(
PrefName.CommentNotificationStore,
null
) ?: listOf()
return list
.sortedByDescending {(it.time / 1000L).toInt()}
.map {
Notification(
it.type.toString(),
System.currentTimeMillis().toInt(),
commentId = it.commentId,
notificationType = it.type.toString(),
mediaId = it.mediaId,
context = it.title + "\n" + it.content,
createdAt = (it.time / 1000L).toInt(),
)
}
}
private fun shouldLoadMore(): Boolean {
val layoutManager = binding.listRecyclerView.layoutManager as LinearLayoutManager
val adapter = binding.listRecyclerView.adapter
return hasNextPage && !binding.followRefresh.isVisible && adapter?.itemCount != 0 &&
layoutManager.findLastVisibleItemPosition() == (adapter!!.itemCount - 1) &&
!binding.listRecyclerView.canScrollVertically(1)
}
fun onClick(
id: Int,
optional: Int?,
type: NotificationClickType
) {
when (type) {
NotificationClickType.USER -> {
ContextCompat.startActivity(
requireContext(), Intent( requireContext(), ProfileActivity::class.java)
.putExtra("userId", id), null
)
}
NotificationClickType.MEDIA -> {
ContextCompat.startActivity(
requireContext(), Intent( requireContext(), MediaDetailsActivity::class.java)
.putExtra("mediaId", id), null
)
}
NotificationClickType.ACTIVITY -> {
ContextCompat.startActivity(
requireContext(), Intent( requireContext(), FeedActivity::class.java)
.putExtra("activityId", id), null
)
}
NotificationClickType.COMMENT -> {
ContextCompat.startActivity(
requireContext(), Intent( requireContext(), MediaDetailsActivity::class.java)
.putExtra("FRAGMENT_TO_LOAD", "COMMENTS")
.putExtra("mediaId", id)
.putExtra("commentId", optional ?: -1),
null
)
}
NotificationClickType.UNDEFINED -> {
// Do nothing
}
}
}
override fun onResume() {
super.onResume()
if (this::binding.isInitialized) {
binding.root.requestLayout()
binding.root.setBaseline((activity as NotificationActivity).navBar)
}
}
companion object {
enum class NotificationClickType {
USER, MEDIA, ACTIVITY, COMMENT, UNDEFINED
}
}
}

View File

@@ -0,0 +1,353 @@
package ani.dantotsu.profile.notification
import android.view.View
import android.view.ViewGroup
import ani.dantotsu.R
import ani.dantotsu.blurImage
import ani.dantotsu.connections.anilist.api.Notification
import ani.dantotsu.connections.anilist.api.NotificationType
import ani.dantotsu.databinding.ItemNotificationBinding
import ani.dantotsu.loadImage
import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationClickType
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.setAnimation
import ani.dantotsu.toPx
import com.xwray.groupie.viewbinding.BindableItem
class NotificationItem(
private val notification: Notification,
val clickCallback: (Int, Int?, NotificationClickType) -> Unit
) : BindableItem<ItemNotificationBinding>() {
private lateinit var binding: ItemNotificationBinding
override fun bind(viewBinding: ItemNotificationBinding, position: Int) {
binding = viewBinding
setAnimation(binding.root.context, binding.root)
setBinding()
}
override fun getLayout(): Int {
return R.layout.item_notification
}
override fun initializeViewBinding(view: View): ItemNotificationBinding {
return ItemNotificationBinding.bind(view)
}
private fun image(user: Boolean = false, commentNotification: Boolean = false, newRelease: Boolean = false) {
val cover = if (user) notification.user?.bannerImage
?: notification.user?.avatar?.medium else notification.media?.bannerImage
?: notification.media?.coverImage?.large
blurImage(binding.notificationBannerImage, if (newRelease) notification.banner else cover)
val defaultHeight = 153.toPx
val userHeight = 90.toPx
val textMarginStart = 125.toPx
if (user) {
binding.notificationCover.visibility = View.GONE
binding.notificationCoverUser.visibility = View.VISIBLE
binding.notificationCoverUserContainer.visibility = View.VISIBLE
if (commentNotification) {
binding.notificationCoverUser.setImageResource(R.drawable.ic_dantotsu_round)
binding.notificationCoverUser.scaleX = 1.4f
binding.notificationCoverUser.scaleY = 1.4f
} else {
binding.notificationCoverUser.loadImage(notification.user?.avatar?.large)
}
binding.notificationBannerImage.layoutParams.height = userHeight
binding.notificationGradiant.layoutParams.height = userHeight
(binding.notificationTextContainer.layoutParams as ViewGroup.MarginLayoutParams).marginStart =
userHeight
} else {
binding.notificationCover.visibility = View.VISIBLE
binding.notificationCoverUser.visibility = View.VISIBLE
binding.notificationCoverUserContainer.visibility = View.GONE
binding.notificationCover.loadImage(if (newRelease) notification.image else notification.media?.coverImage?.large)
binding.notificationBannerImage.layoutParams.height = defaultHeight
binding.notificationGradiant.layoutParams.height = defaultHeight
(binding.notificationTextContainer.layoutParams as ViewGroup.MarginLayoutParams).marginStart =
textMarginStart
}
}
private fun setBinding() {
val notificationType: NotificationType =
NotificationType.valueOf(notification.notificationType)
binding.notificationText.text = ActivityItemBuilder.getContent(notification)
binding.notificationDate.text = ActivityItemBuilder.getDateTime(notification.createdAt)
when (notificationType) {
NotificationType.ACTIVITY_MESSAGE -> {
binding.notificationCover.loadImage(notification.user?.avatar?.large)
image(true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.activityId ?: 0, null, NotificationClickType.ACTIVITY
)
}
}
NotificationType.ACTIVITY_REPLY -> {
binding.notificationCover.loadImage(notification.user?.avatar?.large)
image(true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.activityId ?: 0, null, NotificationClickType.ACTIVITY
)
}
}
NotificationType.FOLLOWING -> {
binding.notificationCover.loadImage(notification.user?.avatar?.large)
image(true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.userId ?: 0, null, NotificationClickType.USER
)
}
}
NotificationType.ACTIVITY_MENTION -> {
binding.notificationCover.loadImage(notification.user?.avatar?.large)
image(true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.activityId ?: 0, null, NotificationClickType.ACTIVITY
)
}
}
NotificationType.THREAD_COMMENT_MENTION -> {
binding.notificationCover.loadImage(notification.user?.avatar?.large)
image(true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
}
NotificationType.THREAD_SUBSCRIBED -> {
binding.notificationCover.loadImage(notification.user?.avatar?.large)
image(true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
}
NotificationType.THREAD_COMMENT_REPLY -> {
binding.notificationCover.loadImage(notification.user?.avatar?.large)
image(true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
}
NotificationType.AIRING -> {
binding.notificationCover.loadImage(notification.media?.coverImage?.large)
image()
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.media?.id ?: 0, null, NotificationClickType.MEDIA
)
}
}
NotificationType.ACTIVITY_LIKE -> {
image(true)
binding.notificationCover.loadImage(notification.user?.avatar?.large)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.activityId ?: 0, null, NotificationClickType.ACTIVITY
)
}
}
NotificationType.ACTIVITY_REPLY_LIKE -> {
binding.notificationCover.loadImage(notification.user?.avatar?.large)
image(true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.activityId ?: 0, null, NotificationClickType.ACTIVITY
)
}
}
NotificationType.THREAD_LIKE -> {
binding.notificationCover.loadImage(notification.user?.avatar?.large)
image(true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
}
NotificationType.THREAD_COMMENT_LIKE -> {
binding.notificationCover.loadImage(notification.user?.avatar?.large)
image(true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
}
NotificationType.ACTIVITY_REPLY_SUBSCRIBED -> {
binding.notificationCover.loadImage(notification.user?.avatar?.large)
image(true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.user?.id ?: 0, null, NotificationClickType.USER
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.activityId ?: 0, null, NotificationClickType.ACTIVITY
)
}
}
NotificationType.RELATED_MEDIA_ADDITION -> {
binding.notificationCover.loadImage(notification.media?.coverImage?.large)
image()
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.media?.id ?: 0, null, NotificationClickType.MEDIA
)
}
}
NotificationType.MEDIA_DATA_CHANGE -> {
binding.notificationCover.loadImage(notification.media?.coverImage?.large)
image()
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.media?.id ?: 0, null, NotificationClickType.MEDIA
)
}
}
NotificationType.MEDIA_MERGE -> {
binding.notificationCover.loadImage(notification.media?.coverImage?.large)
image()
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.media?.id ?: 0, null, NotificationClickType.MEDIA
)
}
}
NotificationType.MEDIA_DELETION -> {
binding.notificationCover.visibility = View.GONE
}
NotificationType.COMMENT_REPLY -> {
image(user = true, commentNotification = true)
if (notification.commentId != null && notification.mediaId != null) {
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.mediaId,
notification.commentId,
NotificationClickType.COMMENT
)
}
}
}
NotificationType.COMMENT_WARNING -> {
image(user = true, commentNotification = true)
if (notification.commentId != null && notification.mediaId != null) {
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.mediaId,
notification.commentId,
NotificationClickType.COMMENT
)
}
}
}
NotificationType.DANTOTSU_UPDATE -> {
image(user = true)
}
NotificationType.SUBSCRIPTION -> {
image(newRelease = true)
binding.notificationCoverUser.setOnClickListener {
clickCallback(
notification.mediaId ?: 0, null, NotificationClickType.MEDIA
)
}
binding.notificationBannerImage.setOnClickListener {
clickCallback(
notification.mediaId ?: 0, null, NotificationClickType.MEDIA
)
}
}
}
}
}