-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20809 from wordpress-mobile/task/20715-extract-mi…
…lestone-fragment [Notifications Refresh] 🤖 Milestone Details: Badge and Title
- Loading branch information
Showing
12 changed files
with
495 additions
and
176 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
228 changes: 228 additions & 0 deletions
228
WordPress/src/main/java/org/wordpress/android/ui/notifications/MilestoneDetailFragment.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
139 changes: 139 additions & 0 deletions
139
WordPress/src/main/java/org/wordpress/android/ui/notifications/NoteBlockTextClickListener.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.