Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🤖 Bring the user straight to the comments reader view #20186

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ import org.wordpress.android.ui.notifications.adapters.NotesAdapter.DataLoadedLi
import org.wordpress.android.ui.notifications.adapters.NotesAdapter.FILTERS
import org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter
import org.wordpress.android.ui.notifications.utils.NotificationsActions
import org.wordpress.android.ui.reader.ReaderActivityLauncher
import org.wordpress.android.ui.reader.comments.ThreadedCommentsActionSource
import org.wordpress.android.util.AniUtils
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.AppLog.T
Expand Down Expand Up @@ -227,9 +229,24 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l
}
incrementInteractions(APP_REVIEWS_EVENT_INCREMENTED_BY_CHECKING_NOTIFICATION)

// 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)
viewModel.openNote(
noteId,
{ siteId, postId, commentId ->
ReaderActivityLauncher.showReaderComments(
activity,
siteId,
postId,
commentId,
ThreadedCommentsActionSource.COMMENT_NOTIFICATION.sourceDescription
)
},
{
// 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 the
// NotificationsDetailListFragment).
openNoteForReply(activity, noteId, filter = notesAdapter?.currentFilter)
}
)
}
}
private val mOnScrollListener: OnScrollListener = object : OnScrollListener() {
Expand Down Expand Up @@ -517,10 +534,10 @@ class NotificationsListFragmentPage : ViewPagerFragment(R.layout.notifications_l
fun openNoteForReply(
activity: Activity?,
noteId: String?,
shouldShowKeyboard: Boolean,
replyText: String?,
filter: FILTERS?,
isTappedFromPushNotification: Boolean
shouldShowKeyboard: Boolean = false,
replyText: String? = null,
filter: FILTERS? = null,
isTappedFromPushNotification: Boolean = false,
) {
if (noteId == null || activity == null || activity.isFinishing) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import org.greenrobot.eventbus.EventBus
import org.wordpress.android.datasets.NotificationsTable
import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper
import org.wordpress.android.models.Note
import org.wordpress.android.models.Notification.PostNotification
import org.wordpress.android.modules.UI_THREAD
Expand All @@ -16,7 +17,10 @@ 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.notifications.utils.NotificationsUtilsWrapper
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.reader.actions.ReaderActions
import org.wordpress.android.ui.reader.actions.ReaderPostActionsWrapper
import org.wordpress.android.util.JetpackBrandingUtils
import org.wordpress.android.viewmodel.Event
import org.wordpress.android.viewmodel.ScopedViewModel
Expand All @@ -29,9 +33,11 @@ class NotificationsListViewModel @Inject constructor(
private val appPrefsWrapper: AppPrefsWrapper,
private val jetpackBrandingUtils: JetpackBrandingUtils,
private val jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil,
private val gcmMessageHandler: GCMMessageHandler

) : ScopedViewModel(mainDispatcher) {
private val gcmMessageHandler: GCMMessageHandler,
private val notificationsUtilsWrapper: NotificationsUtilsWrapper,
private val readerPostTableWrapper: ReaderPostTableWrapper,
private val readerPostActionsWrapper: ReaderPostActionsWrapper,
) : ScopedViewModel(mainDispatcher) {
private val _showJetpackPoweredBottomSheet = MutableLiveData<Event<Boolean>>()
val showJetpackPoweredBottomSheet: LiveData<Event<Boolean>> = _showJetpackPoweredBottomSheet

Expand Down Expand Up @@ -81,6 +87,35 @@ class NotificationsListViewModel @Inject constructor(
}
}

fun openNote(
noteId: String?,
openInTheReader: (siteId: Long, postId: Long, commentId: Long) -> Unit,
openDetailView: () -> Unit
) {
val note = noteId?.let { notificationsUtilsWrapper.getNoteById(noteId) }
if (note != null && note.isCommentType && !note.canModerate()) {
val readerPost = readerPostTableWrapper.getBlogPost(note.siteId.toLong(), note.postId.toLong(), false)
if (readerPost != null) {
openInTheReader(note.siteId.toLong(), note.postId.toLong(), note.commentId)
} else {
readerPostActionsWrapper.requestBlogPost(
note.siteId.toLong(),
note.postId.toLong(),
object : ReaderActions.OnRequestListener<String> {
override fun onSuccess(result: String?) {
openInTheReader(note.siteId.toLong(), note.postId.toLong(), note.commentId)
}

override fun onFailure(statusCode: Int) {
openDetailView()
mkevins marked this conversation as resolved.
Show resolved Hide resolved
}
})
}
} else {
openDetailView()
}
}

sealed class InlineActionEvent {
data class SharePostButtonTapped(val notification: PostNotification): InlineActionEvent()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package org.wordpress.android.ui.notifications

import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.wordpress.android.BaseUnitTest
import org.wordpress.android.datasets.wrappers.ReaderPostTableWrapper
import org.wordpress.android.models.Note
import org.wordpress.android.models.ReaderPost
import org.wordpress.android.push.GCMMessageHandler
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil
import org.wordpress.android.ui.notifications.utils.NotificationsUtilsWrapper
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.reader.actions.ReaderActions
import org.wordpress.android.ui.reader.actions.ReaderPostActionsWrapper
import org.wordpress.android.util.JetpackBrandingUtils

private const val REQUEST_BLOG_LISTENER_PARAM_POSITION = 2

@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class NotificationsListViewModelTest : BaseUnitTest() {
@Mock
private lateinit var appPrefsWrapper: AppPrefsWrapper

@Mock
private lateinit var jetpackBrandingUtils: JetpackBrandingUtils

@Mock
private lateinit var jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil

@Mock
private lateinit var gcmMessageHandler: GCMMessageHandler

@Mock
private lateinit var notificationsUtilsWrapper: NotificationsUtilsWrapper

@Mock
private lateinit var readerPostTableWrapper: ReaderPostTableWrapper

@Mock
private lateinit var readerPostActionsWrapper: ReaderPostActionsWrapper

@Mock
private lateinit var action: ActionHandler

private lateinit var viewModel: NotificationsListViewModel

@Before
fun setup() {
viewModel = NotificationsListViewModel(
testDispatcher(),
appPrefsWrapper,
jetpackBrandingUtils,
jetpackFeatureRemovalOverlayUtil,
gcmMessageHandler,
notificationsUtilsWrapper,
readerPostTableWrapper,
readerPostActionsWrapper
)
}

@Test
fun `WHEN the note cannot be retrieved THEN try opening the detail view`() {
// Given
val noteId = "1"
whenever(notificationsUtilsWrapper.getNoteById(noteId)).thenReturn(null)

// When
viewModel.openNote(noteId, action::openInTheReader, action::openDetailView)

// Then
verify(action, times(0)).openInTheReader(any(), any(), any())
verify(action, times(1)).openDetailView()
}

@Test
fun `WHEN the note is not a comment THEN open detail view`() {
// Given
val noteId = "1"
val note = mock<Note>()
whenever(notificationsUtilsWrapper.getNoteById(noteId)).thenReturn(note)
whenever(note.isCommentType).thenReturn(false)

// When
viewModel.openNote(noteId, action::openInTheReader, action::openDetailView)

// Then
verify(action, times(0)).openInTheReader(any(), any(), any())
verify(action, times(1)).openDetailView()
}

@Test
fun `WHEN the note is a comment that can be moderated THEN open detail view`() {
// Given
val noteId = "1"
val note = mock<Note>()
whenever(notificationsUtilsWrapper.getNoteById(noteId)).thenReturn(note)
whenever(note.isCommentType).thenReturn(true)
whenever(note.canModerate()).thenReturn(true)

// When
viewModel.openNote(noteId, action::openInTheReader, action::openDetailView)

// Then
verify(action, times(0)).openInTheReader(any(), any(), any())
verify(action, times(1)).openDetailView()
}

@Test
fun `WHEN the note is a comment that cannot be moderated and the reader post exists THEN open in reader`() {
// Given
val noteId = "1"
val siteId = 1L
val postId = 2L
val commentId = 3L
val note = mock<Note>()
val readerPost = mock<ReaderPost>()
whenever(notificationsUtilsWrapper.getNoteById(noteId)).thenReturn(note)
whenever(note.siteId).thenReturn(siteId.toInt())
whenever(note.postId).thenReturn(postId.toInt())
whenever(note.commentId).thenReturn(commentId)
whenever(note.isCommentType).thenReturn(true)
whenever(note.canModerate()).thenReturn(false)
whenever(readerPostTableWrapper.getBlogPost(siteId, postId, false)).thenReturn(readerPost)

// When
viewModel.openNote(noteId, action::openInTheReader, action::openDetailView)

// Then
verify(action, times(1)).openInTheReader(siteId, postId, commentId)
verify(action, times(0)).openDetailView()
}

@Test
fun `WHEN the note is a comment that cannot be moderated and the reader post is retrieved THEN open in reader`() {
// Given
val noteId = "1"
val siteId = 1L
val postId = 2L
val commentId = 3L
val note = mock<Note>()
whenever(notificationsUtilsWrapper.getNoteById(noteId)).thenReturn(note)
whenever(note.siteId).thenReturn(siteId.toInt())
whenever(note.postId).thenReturn(postId.toInt())
whenever(note.commentId).thenReturn(commentId)
whenever(note.isCommentType).thenReturn(true)
whenever(note.canModerate()).thenReturn(false)
whenever(readerPostTableWrapper.getBlogPost(siteId, postId, false)).thenReturn(null)
whenever(readerPostActionsWrapper.requestBlogPost(any(), any(), any())).then {
(it.arguments[REQUEST_BLOG_LISTENER_PARAM_POSITION] as ReaderActions.OnRequestListener<*>)
.onSuccess(null)
}

// When
viewModel.openNote(noteId, action::openInTheReader, action::openDetailView)

// Then
verify(action, times(1)).openInTheReader(siteId, postId, commentId)
verify(action, times(0)).openDetailView()
}

@Test
fun `WHEN the comment note cannot be moderated and the reader post retrieval fails THEN open detail view`() {
// Given
val noteId = "1"
val siteId = 1L
val postId = 2L
val note = mock<Note>()
whenever(notificationsUtilsWrapper.getNoteById(noteId)).thenReturn(note)
whenever(note.siteId).thenReturn(siteId.toInt())
whenever(note.postId).thenReturn(postId.toInt())
whenever(note.isCommentType).thenReturn(true)
whenever(note.canModerate()).thenReturn(false)
whenever(readerPostTableWrapper.getBlogPost(siteId, postId, false)).thenReturn(null)
whenever(readerPostActionsWrapper.requestBlogPost(any(), any(), any())).then {
(it.arguments[REQUEST_BLOG_LISTENER_PARAM_POSITION] as ReaderActions.OnRequestListener<*>)
.onFailure(500)
}

// When
viewModel.openNote(noteId, action::openInTheReader, action::openDetailView)

// Then
verify(action, times(0)).openInTheReader(any(), any(), any())
verify(action, times(1)).openDetailView()
}

private class ActionHandler {
fun openInTheReader(siteId: Long, postId: Long, commentId: Long) {
println("openInTheReader($siteId, $postId, $commentId)")
antonis marked this conversation as resolved.
Show resolved Hide resolved
}

fun openDetailView() {
println("openDetailView")
}
}
}
Loading