Skip to content

Commit

Permalink
Merge pull request #20809 from wordpress-mobile/task/20715-extract-mi…
Browse files Browse the repository at this point in the history
…lestone-fragment

[Notifications Refresh] 🤖 Milestone Details: Badge and Title
  • Loading branch information
jarvislin authored May 31, 2024
2 parents e52ef49 + b187f85 commit 9f6e573
Show file tree
Hide file tree
Showing 12 changed files with 495 additions and 176 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import org.wordpress.android.ui.mlp.ModalLayoutPickerFragment;
import org.wordpress.android.ui.mysite.MySiteFragment;
import org.wordpress.android.ui.notifications.DismissNotificationReceiver;
import org.wordpress.android.ui.notifications.MilestoneDetailFragment;
import org.wordpress.android.ui.notifications.NotificationsDetailActivity;
import org.wordpress.android.ui.notifications.NotificationsDetailListFragment;
import org.wordpress.android.ui.notifications.NotificationsListFragmentPage;
Expand Down Expand Up @@ -303,6 +304,8 @@ public interface AppComponent {

void inject(NotificationsDetailListFragment object);

void inject(MilestoneDetailFragment object);

void inject(ReaderSubsActivity object);

void inject(ReaderUpdateLogic object);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package org.wordpress.android.ui.notifications

import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.fragment.app.ListFragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONException
import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.datasets.NotificationsTable
import org.wordpress.android.models.Note
import org.wordpress.android.modules.IO_THREAD
import org.wordpress.android.modules.UI_THREAD
import org.wordpress.android.ui.ScrollableViewInitializedListener
import org.wordpress.android.ui.ViewPagerFragment.Companion.restoreOriginalViewId
import org.wordpress.android.ui.ViewPagerFragment.Companion.setUniqueIdToView
import org.wordpress.android.ui.notifications.adapters.NoteBlockAdapter
import org.wordpress.android.ui.notifications.blocks.MilestoneNoteBlock
import org.wordpress.android.ui.notifications.utils.NotificationsUtilsWrapper
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.AppLog.T.NOTIFS
import org.wordpress.android.util.ToastUtils
import org.wordpress.android.util.image.ImageManager
import javax.inject.Inject
import javax.inject.Named

class MilestoneDetailFragment : ListFragment(), NotificationFragment {
private var restoredListPosition = 0
private var notification: Note? = null
private var rootLayout: LinearLayout? = null
private var restoredNoteId: String? = null
private var noteBlockAdapter: NoteBlockAdapter? = null

@Inject
lateinit var imageManager: ImageManager

@Inject
lateinit var notificationsUtilsWrapper: NotificationsUtilsWrapper

@Inject
@Named(IO_THREAD)
lateinit var ioDispatcher: CoroutineDispatcher

@Inject
@Named(UI_THREAD)
lateinit var mainDispatcher: CoroutineDispatcher

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(requireActivity().application as WordPress).component().inject(this)
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NOTE_ID)) {
restoredNoteId = savedInstanceState.getString(KEY_NOTE_ID)
restoredListPosition = savedInstanceState.getInt(KEY_LIST_POSITION, 0)
} else {
arguments?.let {
setNote(it.getString(KEY_NOTE_ID)) {
reloadNoteBlocks()
}
}
}
}

override fun onSaveInstanceState(outState: Bundle) {
notification?.let {
outState.putString(KEY_NOTE_ID, it.id)
outState.putInt(KEY_LIST_POSITION, listView.firstVisiblePosition)
} ?: run {
// This is done so the fragments pre-loaded by the view pager can store the already rescued restoredNoteId
if (!TextUtils.isEmpty(restoredNoteId)) {
outState.putString(KEY_NOTE_ID, restoredNoteId)
}
}

super.onSaveInstanceState(outState)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.notifications_fragment_detail_list, container, false)
rootLayout = view.findViewById(R.id.notifications_list_root)
return view
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val listView = listView
listView.divider = null
listView.dividerHeight = 0
listView.setHeaderDividersEnabled(false)
}

override fun onResume() {
super.onResume()
setUniqueIdToView(listView)
if (activity is ScrollableViewInitializedListener) {
(activity as ScrollableViewInitializedListener).onScrollableViewInitialized(listView.id)
}

// Set the note if we retrieved the noteId from savedInstanceState
if (!TextUtils.isEmpty(restoredNoteId)) {
setNote(restoredNoteId) {
reloadNoteBlocks()
restoredNoteId = null
}
}
}

override fun onPause() {
restoreOriginalViewId(listView)
super.onPause()
}

private fun setNote(noteId: String?, onNoteSet: (() -> Unit)? = null) {
if (noteId == null) {
showErrorToastAndFinish()
return
}
lifecycleScope.launch(ioDispatcher) {
val note: Note? = NotificationsTable.getNoteById(noteId)
withContext(mainDispatcher) {
if (note == null) {
showErrorToastAndFinish()
} else {
notification = note
onNoteSet?.invoke()
}
}
}
}

private fun showErrorToastAndFinish() {
AppLog.e(NOTIFS, "Note could not be found.")
activity?.let {
ToastUtils.showToast(activity, R.string.error_notification_open)
it.finish()
}
}

private fun reloadNoteBlocks() {
lifecycleScope.launch(ioDispatcher) {
notification?.let { note ->
val noteBlocks = noteBlocksLoader.loadNoteBlocks(note)
withContext(mainDispatcher) {
noteBlocksLoader.handleNoteBlocks(noteBlocks)
}
}
}
}

private val mOnNoteBlockTextClickListener = NoteBlockTextClickListener(this, notification)

// Loop through the 'body' items in this note, and create blocks for each.
private val noteBlocksLoader = object {
private fun addNotesBlock(noteList: MutableList<MilestoneNoteBlock>, bodyArray: JSONArray) {
var i = 0
while (i < bodyArray.length()) {
try {
val noteObject = notificationsUtilsWrapper
.mapJsonToFormattableContent(bodyArray.getJSONObject(i))

val noteBlock = MilestoneNoteBlock(
noteObject, imageManager, notificationsUtilsWrapper,
mOnNoteBlockTextClickListener
)
preloadImage(noteBlock)
noteList.add(noteBlock)
} catch (e: JSONException) {
AppLog.e(NOTIFS, "Error parsing milestone note data.")
}
i++
}
}

private fun preloadImage(noteBlock: MilestoneNoteBlock) {
if (noteBlock.hasImageMediaItem()) {
noteBlock.noteMediaItem?.url?.let {
imageManager.preload(requireContext(), it)
}
}
}

fun loadNoteBlocks(note: Note): List<MilestoneNoteBlock> {
val bodyArray = note.body
val noteList: MutableList<MilestoneNoteBlock> = ArrayList()

if (bodyArray.length() > 0) {
addNotesBlock(noteList, bodyArray)
}
return noteList
}

fun handleNoteBlocks(noteList: List<MilestoneNoteBlock>?) {
if (!isAdded || noteList == null) {
return
}
if (noteBlockAdapter == null) {
noteBlockAdapter = NoteBlockAdapter(requireContext(), noteList)
listAdapter = noteBlockAdapter
} else {
noteBlockAdapter?.setNoteList(noteList)
}
if (restoredListPosition > 0) {
listView.setSelectionFromTop(restoredListPosition, 0)
restoredListPosition = 0
}
}
}

companion object {
private const val KEY_NOTE_ID = "noteId"
private const val KEY_LIST_POSITION = "listPosition"

@JvmStatic
fun newInstance(noteId: String?): MilestoneDetailFragment {
val fragment = MilestoneDetailFragment()
val bundle = Bundle().apply { putString(KEY_NOTE_ID, noteId) }
fragment.arguments = bundle
return fragment
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
@file:Suppress("DEPRECATION")

package org.wordpress.android.ui.notifications

import android.text.TextUtils
import android.view.View
import androidx.fragment.app.Fragment
import org.wordpress.android.datasets.ReaderPostTable
import org.wordpress.android.fluxc.tools.FormattableRangeType
import org.wordpress.android.models.Note
import org.wordpress.android.ui.comments.CommentDetailFragment
import org.wordpress.android.ui.comments.unified.CommentActionPopupHandler
import org.wordpress.android.ui.notifications.blocks.NoteBlock
import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan
import org.wordpress.android.ui.reader.ReaderActivityLauncher
import org.wordpress.android.ui.reader.comments.ThreadedCommentsActionSource
import org.wordpress.android.ui.reader.utils.ReaderUtils

class NoteBlockTextClickListener(
val fragment: Fragment,
val notification: Note?,
private val onActionClickListener: CommentDetailFragment.OnActionClickListener? = null
) : NoteBlock.OnNoteBlockTextClickListener {
override fun onNoteBlockTextClicked(clickedSpan: NoteBlockClickableSpan?) {
if (!fragment.isAdded || fragment.activity !is NotificationsDetailActivity) {
return
}
clickedSpan?.let { handleNoteBlockSpanClick(fragment.activity as NotificationsDetailActivity, it) }
}

override fun showDetailForNoteIds() {
if (!fragment.isAdded || notification == null || fragment.activity !is NotificationsDetailActivity) {
return
}
val detailActivity = fragment.activity as NotificationsDetailActivity

requireNotNull(notification).let { note ->
if (note.isCommentReplyType || !note.isCommentType && note.commentId > 0) {
val commentId = if (note.isCommentReplyType) note.parentCommentId else note.commentId

// show comments list if it exists in the reader
if (ReaderUtils.postAndCommentExists(note.siteId.toLong(), note.postId.toLong(), commentId)) {
detailActivity.showReaderCommentsList(note.siteId.toLong(), note.postId.toLong(), commentId)
} else {
detailActivity.showWebViewActivityForUrl(note.url)
}
} else if (note.isFollowType) {
detailActivity.showBlogPreviewActivity(note.siteId.toLong(), note.isFollowType)
} else {
// otherwise, load the post in the Reader
detailActivity.showPostActivity(note.siteId.toLong(), note.postId.toLong())
}
}
}

override fun showReaderPostComments() {
if (!fragment.isAdded || notification == null || notification.commentId == 0L) {
return
}

requireNotNull(notification).let { note ->
fragment.context?.let { nonNullContext ->
ReaderActivityLauncher.showReaderComments(
nonNullContext, note.siteId.toLong(), note.postId.toLong(),
note.commentId,
ThreadedCommentsActionSource.COMMENT_NOTIFICATION.sourceDescription
)
}
}
}

override fun showSitePreview(siteId: Long, siteUrl: String?) {
if (!fragment.isAdded || notification == null || fragment.activity !is NotificationsDetailActivity) {
return
}
val detailActivity = fragment.activity as NotificationsDetailActivity
if (siteId != 0L) {
detailActivity.showBlogPreviewActivity(siteId, notification.isFollowType)
} else if (!TextUtils.isEmpty(siteUrl)) {
detailActivity.showWebViewActivityForUrl(siteUrl)
}
}

override fun showActionPopup(view: View) {
CommentActionPopupHandler.show(view, onActionClickListener)
}

fun handleNoteBlockSpanClick(
activity: NotificationsDetailActivity,
clickedSpan: NoteBlockClickableSpan
) {
when (clickedSpan.rangeType) {
FormattableRangeType.SITE ->
// Show blog preview
activity.showBlogPreviewActivity(clickedSpan.id, notification?.isFollowType)

FormattableRangeType.USER ->
// Show blog preview
activity.showBlogPreviewActivity(clickedSpan.siteId, notification?.isFollowType)

FormattableRangeType.POST ->
// Show post detail
activity.showPostActivity(clickedSpan.siteId, clickedSpan.id)

FormattableRangeType.COMMENT ->
// Load the comment in the reader list if it exists, otherwise show a webview
if (ReaderUtils.postAndCommentExists(
clickedSpan.siteId, clickedSpan.postId,
clickedSpan.id
)
) {
activity.showReaderCommentsList(
clickedSpan.siteId, clickedSpan.postId,
clickedSpan.id
)
} else {
activity.showWebViewActivityForUrl(clickedSpan.url)
}

FormattableRangeType.SCAN -> activity.showScanActivityForSite(clickedSpan.siteId)
FormattableRangeType.STAT, FormattableRangeType.FOLLOW ->
// We can open native stats if the site is a wpcom or Jetpack sites
activity.showStatsActivityForSite(clickedSpan.siteId, clickedSpan.rangeType)

FormattableRangeType.LIKE -> if (ReaderPostTable.postExists(clickedSpan.siteId, clickedSpan.id)) {
activity.showReaderPostLikeUsers(clickedSpan.siteId, clickedSpan.id)
} else {
activity.showPostActivity(clickedSpan.siteId, clickedSpan.id)
}

FormattableRangeType.REWIND_DOWNLOAD_READY -> activity.showBackupForSite(clickedSpan.siteId)
else ->
// We don't know what type of id this is, let's see if it has a URL and push a webview
if (!TextUtils.isEmpty(clickedSpan.url)) {
activity.showWebViewActivityForUrl(clickedSpan.url)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,8 @@ private Fragment createDetailFragmentForNote(@NonNull Note note) {
note.getSiteId(),
note.getPostId()
);
} else if (NoteExtensions.isAchievement(note)) {
fragment = MilestoneDetailFragment.newInstance(note.getId());
} else {
if (mLikesEnhancementsFeatureConfig.isEnabled() && note.isLikeType()) {
fragment = EngagedPeopleListFragment.newInstance(
Expand Down
Loading

0 comments on commit 9f6e573

Please sign in to comment.