diff --git a/WordPress/src/main/java/org/wordpress/android/models/Note.java b/WordPress/src/main/java/org/wordpress/android/models/Note.java index d7c693696067..1e91894cc6fd 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/Note.java +++ b/WordPress/src/main/java/org/wordpress/android/models/Note.java @@ -17,6 +17,7 @@ import org.wordpress.android.fluxc.model.CommentStatus; import org.wordpress.android.ui.notifications.utils.NotificationsUtilsWrapper; import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.DateTimeUtils; import org.wordpress.android.util.DateUtils; import org.wordpress.android.util.JSONUtils; @@ -522,6 +523,17 @@ public boolean hasLikedComment() { return !(jsonActions == null || jsonActions.length() == 0) && jsonActions.optBoolean(ACTION_KEY_LIKE); } + public void setLikedComment(boolean liked) { + JSONObject jsonActions = getCommentActions(); + if (jsonActions != null) { + try { + jsonActions.put(ACTION_KEY_LIKE, liked); + } catch (JSONException e) { + AppLog.e(T.NOTIFS, "Failed to set 'like' property for the note", e); + } + } + } + public String getUrl() { return queryJSON("url", ""); } diff --git a/WordPress/src/main/java/org/wordpress/android/models/NoteExtensions.kt b/WordPress/src/main/java/org/wordpress/android/models/NoteExtensions.kt index 6685cfffb715..03f6421cc6c8 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/NoteExtensions.kt +++ b/WordPress/src/main/java/org/wordpress/android/models/NoteExtensions.kt @@ -23,6 +23,7 @@ sealed class Notification { NoteType.Like -> Like(url = rawNote.url, title = rawNote.title) NoteType.Reblog -> Reblog(url= rawNote.url, title = rawNote.title) NoteType.NewPost -> NewPost(url= rawNote.url, title = rawNote.title) + NoteType.Comment -> Comment else -> Unknown } } 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 b13ab6fb7a89..149f4308e306 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 @@ -72,7 +72,7 @@ import javax.inject.Inject class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_list_fragment_page), OnScrollToTopListener, DataLoadedListener { - private var notesAdapter: NotesAdapter? = null + private lateinit var notesAdapter: NotesAdapter private var swipeToRefreshHelper: SwipeToRefreshHelper? = null private var isAnimatingOutNewNotificationsBar = false private var shouldRefreshNotifications = false @@ -100,17 +100,6 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l fun onClickNote(noteId: String?) } - @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - val adapter = createOrGetNotesAdapter() - binding?.notificationsList?.adapter = adapter - if (savedInstanceState != null) { - tabPosition = savedInstanceState.getInt(KEY_TAB_POSITION, All.ordinal) - } - (TabPosition.values().getOrNull(tabPosition) ?: All).let { adapter.setFilter(it.filter) } - } - @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == RequestCodes.NOTE_DETAIL) { @@ -136,21 +125,32 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l arguments?.let { tabPosition = it.getInt(KEY_TAB_POSITION, All.ordinal) } + notesAdapter = NotesAdapter( requireActivity(), this, null, + inlineActionEvents = viewModel.inlineActionEvents).apply { + this.setOnNoteClickListener(mOnNoteClickListener) + viewModel.inlineActionEvents.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) + .onEach(::handleInlineActionEvent) + .launchIn(viewLifecycleOwner.lifecycleScope) + } binding = NotificationsListFragmentPageBinding.bind(view).apply { notificationsList.layoutManager = LinearLayoutManager(activity) + notificationsList.adapter = notesAdapter swipeToRefreshHelper = WPSwipeToRefreshHelper.buildSwipeToRefreshHelper(notificationsRefresh) { hideNewNotificationsBar() fetchNotesFromRemote() } layoutNewNotificatons.visibility = View.GONE layoutNewNotificatons.setOnClickListener { onScrollToTop() } + (TabPosition.values().getOrNull(tabPosition) ?: All).let { notesAdapter.setFilter(it.filter) } + } + viewModel.updatedNote.observe(viewLifecycleOwner) { + notesAdapter.updateNote(it) } } override fun onDestroyView() { super.onDestroyView() - notesAdapter?.cancelReloadNotesTask() - notesAdapter = null + notesAdapter.cancelReloadNotesTask() swipeToRefreshHelper = null binding?.notificationsList?.adapter = null binding?.notificationsList?.removeCallbacks(showNewUnseenNotificationsRunnable) @@ -187,7 +187,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l binding?.hideNewNotificationsBar() EventBus.getDefault().post(NotificationsUnseenStatus(false)) if (accountStore.hasAccessToken()) { - notesAdapter!!.reloadNotesFromDBAsync() + notesAdapter.reloadNotesFromDBAsync() if (shouldRefreshNotifications) { fetchNotesFromRemote() } @@ -234,7 +234,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l // Open the latest version of this note in case it has changed, which can happen if the note was tapped // from the list after it was updated by another fragment (such as NotificationsDetailListFragment). - openNoteForReply(activity, noteId, false, null, notesAdapter!!.currentFilter, false) + openNoteForReply(activity, noteId, false, null, notesAdapter.currentFilter, false) } } private val mOnScrollListener: OnScrollListener = object : OnScrollListener() { @@ -253,7 +253,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l } private fun fetchNotesFromRemote() { - if (!isAdded || notesAdapter == null) { + if (!isAdded) { return } if (!NetworkUtils.isNetworkAvailable(activity)) { @@ -414,17 +414,6 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l } } - private fun createOrGetNotesAdapter(): NotesAdapter { - return notesAdapter ?: NotesAdapter( requireActivity(), this, null, - inlineActionEvents = viewModel.inlineActionEvents).apply { - notesAdapter = this - this.setOnNoteClickListener(mOnNoteClickListener) - viewModel.inlineActionEvents.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) - .onEach(::handleInlineActionEvent) - .launchIn(viewLifecycleOwner.lifecycleScope) - } - } - private fun handleInlineActionEvent(actionEvent: InlineActionEvent) { analyticsTrackerWrapper.track(NOTIFICATIONS_INLINE_ACTION_TAPPED, mapOf( InlineActionEvent.KEY_INLINE_ACTION to actionEvent::class.simpleName @@ -435,6 +424,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l ActivityLauncher.openShareIntent(it, postNotification.url, postNotification.title) } } + is InlineActionEvent.LikeCommentButtonTapped -> viewModel.likeComment(actionEvent.note, actionEvent.liked) } } @@ -443,7 +433,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l * Mark notifications as read in CURRENT tab, use filteredNotes instead of notes */ fun markAllNotesAsRead() { - viewModel.markNoteAsRead(requireContext(), createOrGetNotesAdapter().filteredNotes) + viewModel.markNoteAsRead(requireContext(), notesAdapter.filteredNotes) } @Subscribe(sticky = true, threadMode = MAIN) @@ -468,7 +458,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l if (!isAdded) { return } - notesAdapter!!.reloadNotesFromDBAsync() + notesAdapter.reloadNotesFromDBAsync() if (event.hasUnseenNotes) { binding?.showNewUnseenNotificationsUI() } @@ -480,7 +470,7 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l return } swipeToRefreshHelper?.isRefreshing = false - notesAdapter!!.addAll(event.notes, true) + notesAdapter.addAll(event.notes, true) } @Suppress("unused", "UNUSED_PARAMETER") diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt index 342baf99e206..52c3f4f6ef39 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt @@ -8,16 +8,18 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableSharedFlow import org.greenrobot.eventbus.EventBus import org.wordpress.android.datasets.NotificationsTable +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.CommentsStore +import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.models.Note import org.wordpress.android.models.Notification.PostNotification -import org.wordpress.android.modules.UI_THREAD +import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.push.GCMMessageHandler import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil import org.wordpress.android.ui.jetpackoverlay.JetpackOverlayConnectedFeature.NOTIFICATIONS import org.wordpress.android.ui.notifications.NotificationEvents.NotificationsChanged import org.wordpress.android.ui.notifications.utils.NotificationsActions import org.wordpress.android.ui.prefs.AppPrefsWrapper -import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject @@ -25,32 +27,27 @@ import javax.inject.Named @HiltViewModel class NotificationsListViewModel @Inject constructor( - @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, + @Named(BG_THREAD) bgDispatcher: CoroutineDispatcher, private val appPrefsWrapper: AppPrefsWrapper, - private val jetpackBrandingUtils: JetpackBrandingUtils, private val jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil, - private val gcmMessageHandler: GCMMessageHandler - -) : ScopedViewModel(mainDispatcher) { + private val gcmMessageHandler: GCMMessageHandler, + private val siteStore: SiteStore, + private val commentStore: CommentsStore +) : ScopedViewModel(bgDispatcher) { private val _showJetpackPoweredBottomSheet = MutableLiveData>() val showJetpackPoweredBottomSheet: LiveData> = _showJetpackPoweredBottomSheet private val _showJetpackOverlay = MutableLiveData>() val showJetpackOverlay: LiveData> = _showJetpackOverlay + private val _updatedNote = MutableLiveData() + val updatedNote: LiveData = _updatedNote + val inlineActionEvents = MutableSharedFlow() val isNotificationsPermissionsWarningDismissed get() = appPrefsWrapper.notificationPermissionsWarningDismissed - init { - if (jetpackBrandingUtils.shouldShowJetpackPoweredBottomSheet()) showJetpackPoweredBottomSheet() - } - - private fun showJetpackPoweredBottomSheet() { -// _showJetpackPoweredBottomSheet.value = Event(true) - } - fun onResume() { if (jetpackFeatureRemovalOverlayUtil.shouldShowFeatureSpecificJetpackOverlay(NOTIFICATIONS)) showJetpackOverlay() @@ -81,8 +78,22 @@ class NotificationsListViewModel @Inject constructor( } } + fun likeComment(note: Note, liked: Boolean) = launch { + val site = siteStore.getSiteBySiteId(note.siteId.toLong()) ?: SiteModel().apply { + siteId = note.siteId.toLong() + setIsWPCom(true) + } + note.setLikedComment(liked) + _updatedNote.postValue(note) + val result = commentStore.likeComment(site, note.commentId, null, liked) + if (result.isError.not()) { + NotificationsTable.saveNote(note) + } + } + sealed class InlineActionEvent { - data class SharePostButtonTapped(val notification: PostNotification): InlineActionEvent() + data class SharePostButtonTapped(val notification: PostNotification) : InlineActionEvent() + class LikeCommentButtonTapped(val note: Note, val liked: Boolean) : InlineActionEvent() companion object { const val KEY_INLINE_ACTION = "inline_action" 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 72aaa615a6c0..0d5dd3fc4ad1 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 @@ -4,6 +4,7 @@ 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 @@ -18,6 +19,7 @@ import androidx.annotation.StringRes import androidx.core.text.BidiFormatter import androidx.core.view.ViewCompat import androidx.core.view.isVisible +import androidx.core.widget.ImageViewCompat import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -41,6 +43,7 @@ import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan import org.wordpress.android.ui.notifications.utils.NotificationsUtilsWrapper import org.wordpress.android.util.GravatarUtils import org.wordpress.android.util.RtlUtils +import org.wordpress.android.util.extensions.getColorFromAttribute import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.image.ImageType import javax.inject.Inject @@ -273,7 +276,6 @@ class NotesAdapter( private fun Note.shouldShowMultipleAvatars() = isFollowType || isLikeType || isCommentLikeType - private fun handleMaxLines(subject: TextView, detail: TextView) { subject.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { @@ -305,6 +307,21 @@ class NotesAdapter( reloadNotesFromDBTask!!.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) } + /** + * Update the note in the adapter and notify the change + */ + fun updateNote(note: Note) { + val notePosition = notes.indexOfFirst { it.id == note.id } + if (notePosition != -1) { + notes[notePosition] = note + } + val filteredPosition = filteredNotes.indexOfFirst { it.id == note.id } + if (filteredPosition != -1) { + filteredNotes[filteredPosition] = note + notifyItemChanged(filteredPosition) + } + } + @SuppressLint("StaticFieldLeak") private inner class ReloadNotesFromDBTask : AsyncTask>() { override fun doInBackground(vararg voids: Void?): ArrayList { @@ -359,29 +376,54 @@ class NotesAdapter( @Suppress("ForbiddenComment") fun bindInlineActionIconsForNote(note: Note) = Notification.from(note).let { notification -> when (notification) { - Comment -> { - actionIcon.setImageResource(R.drawable.star_empty) - actionIcon.isVisible = true - actionIcon.setOnClickListener { - // TODO: handle tap on comment's inline action icon (the star) - } - } - is PostNotification -> { - actionIcon.setImageResource(R.drawable.block_share) - actionIcon.isVisible = true - actionIcon.setOnClickListener { - coroutineScope.launch { - inlineActionEvents.emit( - InlineActionEvent.SharePostButtonTapped(notification) - ) - } - } - } + Comment -> bindLikeCommentAction(note) + is PostNotification.NewPost -> bindLikePostAction() + is PostNotification -> bindShareAction(notification) is Unknown -> { actionIcon.isVisible = false } } } + + private fun bindShareAction(notification: PostNotification) { + actionIcon.setImageResource(R.drawable.block_share) + val color = contentView.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) + ImageViewCompat.setImageTintList(actionIcon, ColorStateList.valueOf(color)) + actionIcon.isVisible = true + actionIcon.setOnClickListener { + coroutineScope.launch { + inlineActionEvents.emit( + InlineActionEvent.SharePostButtonTapped(notification) + ) + } + } + } + + private fun bindLikePostAction() { + // implement like post action + } + + private fun bindLikeCommentAction(note: Note) { + if (note.canLike().not()) return + setupLikeIcon(note.hasLikedComment()) + actionIcon.setOnClickListener { + val liked = note.hasLikedComment().not() + setupLikeIcon(liked) + coroutineScope.launch { + inlineActionEvents.emit( + InlineActionEvent.LikeCommentButtonTapped(note, liked) + ) + } + } + } + + private fun setupLikeIcon(liked: Boolean) { + actionIcon.isVisible = true + actionIcon.setImageResource(if (liked) R.drawable.star_filled else R.drawable.star_empty) + val color = if (liked) contentView.context.getColor(R.color.inline_action_filled) + else contentView.context.getColorFromAttribute(R.attr.wpColorOnSurfaceMedium) + ImageViewCompat.setImageTintList(actionIcon, ColorStateList.valueOf(color)) + } } private val onClickListener = View.OnClickListener { view -> @@ -423,15 +465,19 @@ class NotesAdapter( FILTERS.FILTER_COMMENT -> if (currentNote.isCommentType) { filteredNotes.add(currentNote) } + FILTERS.FILTER_FOLLOW -> if (currentNote.isFollowType) { filteredNotes.add(currentNote) } + FILTERS.FILTER_UNREAD -> if (currentNote.isUnread) { filteredNotes.add(currentNote) } + FILTERS.FILTER_LIKE -> if (currentNote.isLikeType) { filteredNotes.add(currentNote) } + else -> Unit } } diff --git a/WordPress/src/main/res/drawable/star_filled.xml b/WordPress/src/main/res/drawable/star_filled.xml new file mode 100644 index 000000000000..84ae3d2e18fd --- /dev/null +++ b/WordPress/src/main/res/drawable/star_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/WordPress/src/main/res/layout/notifications_list_item.xml b/WordPress/src/main/res/layout/notifications_list_item.xml index 716a1fb35d00..640df7fde81b 100644 --- a/WordPress/src/main/res/layout/notifications_list_item.xml +++ b/WordPress/src/main/res/layout/notifications_list_item.xml @@ -63,12 +63,14 @@ android:id="@+id/action" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:foreground="?attr/selectableItemBackgroundBorderless" + android:background="?attr/selectableItemBackgroundBorderless" android:padding="@dimen/notifications_item_action_padding" android:contentDescription="@null" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="@+id/note_avatar" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/note_avatar" + tools:visibility="visible" app:tint="?attr/wpColorOnSurfaceMedium" tools:src="@drawable/star_empty" /> @@ -76,8 +78,9 @@ android:id="@+id/note_subject_container" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/margin_extra_large" + android:layout_marginEnd="@dimen/margin_none" android:layout_marginStart="@dimen/margin_medium_large" + app:layout_goneMarginEnd="@dimen/margin_extra_large" app:layout_constraintEnd_toStartOf="@+id/action" app:layout_constraintStart_toEndOf="@+id/note_avatar" app:layout_constraintTop_toTopOf="parent"> @@ -108,13 +111,15 @@ android:id="@+id/note_detail" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/margin_extra_large" + android:layout_marginEnd="@dimen/margin_none" android:layout_marginStart="@dimen/margin_medium_large" android:ellipsize="end" android:importantForAccessibility="no" android:maxLines="2" + android:textAlignment="viewStart" android:textAppearance="@style/WordPress.TextAppearance.NotificationItemContent" android:visibility="gone" + app:layout_goneMarginEnd="@dimen/margin_extra_large" app:layout_constraintEnd_toStartOf="@id/action" app:layout_constraintStart_toEndOf="@+id/note_avatar" app:layout_constraintTop_toBottomOf="@+id/note_subject_container" diff --git a/WordPress/src/main/res/values-night/colors.xml b/WordPress/src/main/res/values-night/colors.xml index 4fed9a9fe846..045ca78257e4 100644 --- a/WordPress/src/main/res/values-night/colors.xml +++ b/WordPress/src/main/res/values-night/colors.xml @@ -115,4 +115,6 @@ @color/black @color/white + + @color/jetpack_green_30 diff --git a/WordPress/src/main/res/values/colors.xml b/WordPress/src/main/res/values/colors.xml index 0697863412fc..8a503bb522a4 100644 --- a/WordPress/src/main/res/values/colors.xml +++ b/WordPress/src/main/res/values/colors.xml @@ -151,4 +151,7 @@ #DEDEDE #8A8A8E + + + @color/jetpack_green_50