diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt index f578ed92910f..934f213ee0c1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragmentPage.kt @@ -17,8 +17,11 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.OnScrollListener import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode.MAIN @@ -77,7 +80,6 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l private lateinit var notesAdapter: NotesAdapter private var swipeToRefreshHelper: SwipeToRefreshHelper? = null private var isAnimatingOutNewNotificationsBar = false - private var shouldRefreshNotifications = false private var tabPosition = 0 private val viewModel: NotificationsListViewModel by viewModels() @@ -105,7 +107,6 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == RequestCodes.NOTE_DETAIL) { - shouldRefreshNotifications = false if (resultCode == Activity.RESULT_OK) { val noteId = data?.getStringExtra(NotificationsListFragment.NOTE_MODERATE_ID_EXTRA) val newStatus = data?.getStringExtra(NotificationsListFragment.NOTE_MODERATE_STATUS_EXTRA) @@ -119,7 +120,6 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (requireActivity().application as WordPress).component().inject(this) - shouldRefreshNotifications = true } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -139,7 +139,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l notificationsList.adapter = notesAdapter swipeToRefreshHelper = WPSwipeToRefreshHelper.buildSwipeToRefreshHelper(notificationsRefresh) { hideNewNotificationsBar() - fetchNotesFromRemote() + fetchRemoteNotes() } layoutNewNotificatons.visibility = View.GONE layoutNewNotificatons.setOnClickListener { onScrollToTop() } @@ -148,11 +148,18 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l viewModel.updatedNote.observe(viewLifecycleOwner) { notesAdapter.updateNote(it) } + + if (tabPosition == All.ordinal) { + swipeToRefreshHelper?.isRefreshing = true + fetchRemoteNotes() + } else { + notesAdapter.reloadLocalNotes() + } } override fun onDestroyView() { super.onDestroyView() - notesAdapter.cancelReloadNotesTask() + notesAdapter.cancelReloadLocalNotes() swipeToRefreshHelper = null binding?.notificationsList?.adapter = null binding?.notificationsList?.removeCallbacks(showNewUnseenNotificationsRunnable) @@ -175,11 +182,6 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l } } - override fun onPause() { - super.onPause() - shouldRefreshNotifications = true - } - override fun getScrollableViewForUniqueIdProvision(): View? { return binding?.notificationsList } @@ -188,12 +190,6 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l super.onResume() binding?.hideNewNotificationsBar() EventBus.getDefault().post(NotificationsUnseenStatus(false)) - if (accountStore.hasAccessToken()) { - notesAdapter.reloadNotesFromDBAsync() - if (shouldRefreshNotifications) { - fetchNotesFromRemote() - } - } } override fun onSaveInstanceState(outState: Bundle) { @@ -265,11 +261,15 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l private fun NotificationsListFragmentPageBinding.clearPendingNotificationsItemsOnUI() { hideNewNotificationsBar() EventBus.getDefault().post(NotificationsUnseenStatus(false)) - NotificationsActions.updateNotesSeenTimestamp() - Thread { gcmMessageHandler.removeAllNotifications(activity) }.start() + lifecycleScope.launch { + withContext(Dispatchers.IO) { + NotificationsActions.updateNotesSeenTimestamp() + gcmMessageHandler.removeAllNotifications(activity) + } + } } - private fun fetchNotesFromRemote() { + private fun fetchRemoteNotes() { if (!isAdded) { return } @@ -422,12 +422,14 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l } } - private fun updateNote(noteId: String, status: CommentStatus) { - val note = NotificationsTable.getNoteById(noteId) - if (note != null) { - note.localStatus = status.toString() - NotificationsTable.saveNote(note) - EventBus.getDefault().post(NotificationsChanged()) + private fun updateNote(noteId: String, status: CommentStatus) = lifecycleScope.launch { + withContext(Dispatchers.IO) { + val note = NotificationsTable.getNoteById(noteId) + if (note != null) { + note.localStatus = status.toString() + NotificationsTable.saveNote(note) + EventBus.getDefault().post(NotificationsChanged()) + } } } @@ -456,27 +458,31 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l @Subscribe(sticky = true, threadMode = MAIN) fun onEventMainThread(event: NoteLikeOrModerationStatusChanged) { - NotificationsActions.downloadNoteAndUpdateDB( - event.noteId, - { - EventBus.getDefault() - .removeStickyEvent( + lifecycleScope.launch { + withContext(Dispatchers.IO) { + NotificationsActions.downloadNoteAndUpdateDB( + event.noteId, + { + EventBus.getDefault() + .removeStickyEvent( + NoteLikeOrModerationStatusChanged::class.java + ) + } + ) { + EventBus.getDefault().removeStickyEvent( NoteLikeOrModerationStatusChanged::class.java ) + } } - ) { - EventBus.getDefault().removeStickyEvent( - NoteLikeOrModerationStatusChanged::class.java - ) } } - @Subscribe(threadMode = MAIN) + @Subscribe(sticky = true, threadMode = MAIN) fun onEventMainThread(event: NotificationsChanged) { if (!isAdded) { return } - notesAdapter.reloadNotesFromDBAsync() + notesAdapter.reloadLocalNotes() if (event.hasUnseenNotes) { binding?.showNewUnseenNotificationsUI() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt index 29abdf952946..9770c061875d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/adapters/NotesAdapter.kt @@ -2,10 +2,8 @@ package org.wordpress.android.ui.notifications.adapters -import android.annotation.SuppressLint import android.content.Context import android.content.res.ColorStateList -import android.os.AsyncTask import android.text.Spanned import android.text.TextUtils import android.view.LayoutInflater @@ -20,11 +18,14 @@ import androidx.core.text.BidiFormatter import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.core.widget.ImageViewCompat +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.datasets.NotificationsTable @@ -56,6 +57,7 @@ class NotesAdapter( private val textIndentSize: Int private val dataLoadedListener: DataLoadedListener private val onLoadMoreListener: OnLoadMoreListener? + private val coroutineScope = CoroutineScope(Dispatchers.IO) val filteredNotes = ArrayList() @Inject @@ -84,7 +86,7 @@ class NotesAdapter( var currentFilter = FILTERS.FILTER_ALL private set - private var reloadNotesFromDBTask: ReloadNotesFromDBTask? = null + private var reloadLocalNotesJob: Job? = null interface DataLoadedListener { fun onDataLoaded(itemsCount: Int) @@ -99,13 +101,25 @@ class NotesAdapter( currentFilter = newFilter } - @SuppressLint("NotifyDataSetChanged") - fun addAll(notes: List) { - try { - filteredNotes.clear() - filteredNotes.addAll(buildFilteredNotesList(notes, currentFilter)) - } finally { - notifyDataSetChanged() + /** + * Add notes to the adapter and notify the change + */ + fun addAll(notes: List) = coroutineScope.launch { + val newNotes = buildFilteredNotesList(notes, currentFilter) + val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun getOldListSize(): Int = filteredNotes.size + override fun getNewListSize(): Int = newNotes.size + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + filteredNotes[oldItemPosition].id == newNotes[newItemPosition].id + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + filteredNotes[oldItemPosition].json.toString() == newNotes[newItemPosition].json.toString() + }) + + filteredNotes.clear() + filteredNotes.addAll(newNotes) + withContext(Dispatchers.Main) { + result.dispatchUpdatesTo(this@NotesAdapter) dataLoadedListener.onDataLoaded(itemCount) } } @@ -121,7 +135,6 @@ class NotesAdapter( private fun isValidPosition(position: Int): Boolean = position >= 0 && position < filteredNotes.size - override fun getItemCount(): Int = filteredNotes.size private val Note.timeGroup @@ -278,17 +291,19 @@ class NotesAdapter( onNoteClickListener = mNoteClickListener } - fun cancelReloadNotesTask() { - if (reloadNotesFromDBTask != null && reloadNotesFromDBTask!!.status != AsyncTask.Status.FINISHED) { - reloadNotesFromDBTask!!.cancel(true) - reloadNotesFromDBTask = null - } + fun cancelReloadLocalNotes() { + reloadLocalNotesJob?.cancel() } - fun reloadNotesFromDBAsync() { - cancelReloadNotesTask() - reloadNotesFromDBTask = ReloadNotesFromDBTask() - reloadNotesFromDBTask!!.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + /** + * Reload the notes from local database and update the adapter + */ + fun reloadLocalNotes() { + cancelReloadLocalNotes() + reloadLocalNotesJob = coroutineScope.launch { + val notes = NotificationsTable.getLatestNotes() + addAll(notes) + } } /** @@ -302,17 +317,6 @@ class NotesAdapter( } } - @SuppressLint("StaticFieldLeak") - private inner class ReloadNotesFromDBTask : AsyncTask>() { - override fun doInBackground(vararg voids: Void?): ArrayList { - return NotificationsTable.getLatestNotes() - } - - override fun onPostExecute(notes: ArrayList) { - addAll(notes) - } - } - inner class NoteViewHolder(view: View) : RecyclerView.ViewHolder(view) { val contentView: View val headerText: TextView @@ -330,8 +334,6 @@ class NotesAdapter( val unreadNotificationView: View val actionIcon: ImageView - val coroutineScope = CoroutineScope(Dispatchers.Main) - init { contentView = checkNotNull(view.findViewById(R.id.note_content_container)) headerText = checkNotNull(view.findViewById(R.id.header_text))