diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java index 4918c8d671a3..d7113e563bb6 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java +++ b/WordPress/src/main/java/org/wordpress/android/datasets/NotificationsTable.java @@ -80,7 +80,7 @@ private static boolean putNote(Note note, boolean checkBeforeInsert) { String rawNote = prepareNote(note.getId(), note.getJSON().toString()); ContentValues values = new ContentValues(); - values.put("type", note.getType()); + values.put("type", note.getRawType()); values.put("timestamp", note.getTimestamp()); values.put("raw_note_data", rawNote); 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 b87716e86e48..d7c693696067 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/Note.java +++ b/WordPress/src/main/java/org/wordpress/android/models/Note.java @@ -95,62 +95,76 @@ public String getId() { return mKey; } - public String getType() { + @NonNull + public String getRawType() { return queryJSON("type", NOTE_UNKNOWN_TYPE); } - private Boolean isType(String type) { - return getType().equals(type); + @NonNull + private Boolean isTypeRaw(@NonNull String rawType) { + return getRawType().equals(rawType); } + @NonNull public Boolean isCommentType() { synchronized (mSyncLock) { return (isAutomattcherType() && JSONUtils.queryJSON(mNoteJSON, "meta.ids.comment", -1) != -1) - || isType(NOTE_COMMENT_TYPE); + || isTypeRaw(NOTE_COMMENT_TYPE); } } + @NonNull public Boolean isAutomattcherType() { - return isType(NOTE_MATCHER_TYPE); + return isTypeRaw(NOTE_MATCHER_TYPE); } + @NonNull public Boolean isNewPostType() { - return isType(NOTE_NEW_POST_TYPE); + return isTypeRaw(NOTE_NEW_POST_TYPE); } + @NonNull public Boolean isFollowType() { - return isType(NOTE_FOLLOW_TYPE); + return isTypeRaw(NOTE_FOLLOW_TYPE); } + @NonNull public Boolean isLikeType() { return isPostLikeType() || isCommentLikeType(); } + @NonNull public Boolean isPostLikeType() { - return isType(NOTE_LIKE_TYPE); + return isTypeRaw(NOTE_LIKE_TYPE); } + @NonNull public Boolean isCommentLikeType() { - return isType(NOTE_COMMENT_LIKE_TYPE); + return isTypeRaw(NOTE_COMMENT_LIKE_TYPE); } + @NonNull public Boolean isReblogType() { - return isType(NOTE_REBLOG_TYPE); + return isTypeRaw(NOTE_REBLOG_TYPE); } + @NonNull public Boolean isViewMilestoneType() { - return isType(NOTE_VIEW_MILESTONE); + return isTypeRaw(NOTE_VIEW_MILESTONE); } + @NonNull public Boolean isCommentReplyType() { return isCommentType() && getParentCommentId() > 0; } // Returns true if the user has replied to this comment note + @NonNull public Boolean isCommentWithUserReply() { return isCommentType() && !TextUtils.isEmpty(getCommentSubjectNoticon()); } + @NonNull public Boolean isUserList() { return isLikeType() || isFollowType() || isReblogType(); } @@ -278,6 +292,7 @@ public long getCommentReplyId() { /** * Compare note timestamp to now and return a time grouping */ + @NonNull public static NoteTimeGroup getTimeGroupForTimestamp(long timestamp) { Date today = new Date(); Date then = new Date(timestamp * 1000); @@ -422,7 +437,8 @@ public long getParentCommentId() { /** * Rudimentary system for pulling an item out of a JSON object hierarchy */ - private U queryJSON(String query, U defaultObject) { + @NonNull + private U queryJSON(@Nullable String query, @NonNull U defaultObject) { synchronized (mSyncLock) { if (mNoteJSON == null) { return defaultObject; diff --git a/WordPress/src/main/java/org/wordpress/android/models/NoteExtensions.kt b/WordPress/src/main/java/org/wordpress/android/models/NoteExtensions.kt new file mode 100644 index 000000000000..47b9159e7530 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/models/NoteExtensions.kt @@ -0,0 +1,32 @@ +package org.wordpress.android.models + +val Note.type + get() = NoteType.from(rawType) + +sealed class Notification { + data class Like(val url: String, val title: String): Notification() + data object Unknown: Notification() + + companion object { + fun from(rawNote: Note) = when(rawNote.type) { + NoteType.Like -> Like(url= rawNote.url, title = rawNote.title) + else -> Unknown + } + } +} +enum class NoteType(val rawType: String) { + Follow(Note.NOTE_FOLLOW_TYPE), + Like(Note.NOTE_LIKE_TYPE), + Comment(Note.NOTE_COMMENT_TYPE), + Matcher(Note.NOTE_MATCHER_TYPE), + CommentLike(Note.NOTE_COMMENT_LIKE_TYPE), + Reblog(Note.NOTE_REBLOG_TYPE), + NewPost(Note.NOTE_NEW_POST_TYPE), + ViewMilestone(Note.NOTE_VIEW_MILESTONE), + Unknown(Note.NOTE_UNKNOWN_TYPE); + + companion object { + private val map = entries.associateBy(NoteType::rawType) + fun from(rawType: String) = map[rawType] ?: Unknown + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt index 6d02ba429ca6..50f5221b7183 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/ListScenarioUtils.kt @@ -30,7 +30,7 @@ class ListScenarioUtils @Inject constructor( val notificationsUtilsWrapper: NotificationsUtilsWrapper ) { fun mapLikeNoteToListScenario(note: Note, context: Context): ListScenario { - require(note.isLikeType) { "mapLikeNoteToListScenario > unexpected note type ${note.type}" } + require(note.isLikeType) { "mapLikeNoteToListScenario > unexpected note type ${note.rawType}" } val imageType = AVATAR_WITH_BACKGROUND val headerNoteBlock = HeaderNoteBlock( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java index 2881bf40c2ec..b1b04645e9b7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java @@ -222,7 +222,7 @@ private void updateUIAndNote(boolean doRefresh) { // analytics tracking Map properties = new HashMap<>(); - properties.put("notification_type", note.getType()); + properties.put("notification_type", note.getRawType()); AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATIONS_OPENED_NOTIFICATION_DETAILS, properties); setProgressVisible(false); @@ -343,7 +343,7 @@ private void setActionBarTitleForNote(Note note) { String title = note.getTitle(); if (TextUtils.isEmpty(title)) { // set a default title if title is not set within the note - switch (note.getType()) { + switch (note.getRawType()) { case NOTE_FOLLOW_TYPE: title = getString(R.string.follows); break; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt index 54bf71f827ec..f1f5dff7eb96 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt @@ -420,7 +420,7 @@ class NotificationsDetailListFragment : ListFragment(), NotificationFragment { ).also { if (noteObject.ranges != null && noteObject.ranges!!.isNotEmpty()) { val range = noteObject.ranges!![noteObject.ranges!!.size - 1] - it.setClickableSpan(range, note.type) + it.setClickableSpan(range, note.rawType) } } } else { 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 bc5cd431fe55..af54aee9c8a5 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 @@ -14,6 +14,7 @@ import android.view.ViewGroup.MarginLayoutParams import android.view.ViewTreeObserver import android.widget.ImageView import android.widget.TextView +import androidx.annotation.StringRes import androidx.core.text.BidiFormatter import androidx.core.view.ViewCompat import androidx.core.view.isVisible @@ -24,6 +25,8 @@ import org.wordpress.android.datasets.NotificationsTable import org.wordpress.android.models.Note import org.wordpress.android.models.Note.NoteTimeGroup import org.wordpress.android.models.Note.TimeStampComparator +import org.wordpress.android.models.NoteType +import org.wordpress.android.models.type import org.wordpress.android.ui.comments.CommentUtils import org.wordpress.android.ui.notifications.NotificationsListFragmentPage.OnNoteClickListener import org.wordpress.android.ui.notifications.adapters.NotesAdapter.NoteViewHolder @@ -46,13 +49,11 @@ class NotesAdapter( private val notes = ArrayList() val filteredNotes = ArrayList() - @JvmField @Inject - var imageManager: ImageManager? = null + lateinit var imageManager: ImageManager - @JvmField @Inject - var notificationsUtilsWrapper: NotificationsUtilsWrapper? = null + lateinit var notificationsUtilsWrapper: NotificationsUtilsWrapper enum class FILTERS { FILTER_ALL, @@ -128,35 +129,42 @@ class NotesAdapter( return filteredNotes.size } + private val Note.timeGroup + get() = Note.getTimeGroupForTimestamp(timestamp) + + @StringRes + private fun timeGroupHeaderText(note: Note, previousNote: Note?) = + previousNote?.timeGroup.let { previousTimeGroup -> + val timeGroup = note.timeGroup + if (previousTimeGroup?.let { it == timeGroup } == true) { + // If the previous time group exists and is the same, we don't need a new one + null + } else { + // Otherwise, we create a new one + when (timeGroup) { + NoteTimeGroup.GROUP_TODAY -> R.string.stats_timeframe_today + NoteTimeGroup.GROUP_YESTERDAY -> R.string.stats_timeframe_yesterday + NoteTimeGroup.GROUP_OLDER_TWO_DAYS -> R.string.older_two_days + NoteTimeGroup.GROUP_OLDER_WEEK -> R.string.older_last_week + NoteTimeGroup.GROUP_OLDER_MONTH -> R.string.older_month + } + } + } + @Suppress("CyclomaticComplexMethod", "LongMethod") override fun onBindViewHolder(noteViewHolder: NoteViewHolder, position: Int) { val note = getNoteAtPosition(position) ?: return + val previousNote = getNoteAtPosition(position - 1) noteViewHolder.contentView.tag = note.id - // Display group header - val timeGroup = Note.getTimeGroupForTimestamp(note.timestamp) - var previousTimeGroup: NoteTimeGroup? = null - if (position > 0) { - val previousNote = getNoteAtPosition(position - 1) - previousTimeGroup = Note.getTimeGroupForTimestamp( - previousNote!!.timestamp - ) - } - if (previousTimeGroup?.let { it == timeGroup } == true) { - noteViewHolder.headerText.visibility = View.GONE - } else { - noteViewHolder.headerText.visibility = View.VISIBLE - timeGroup?.let { - noteViewHolder.headerText.setText( - when (it) { - NoteTimeGroup.GROUP_TODAY -> R.string.stats_timeframe_today - NoteTimeGroup.GROUP_YESTERDAY -> R.string.stats_timeframe_yesterday - NoteTimeGroup.GROUP_OLDER_TWO_DAYS -> R.string.older_two_days - NoteTimeGroup.GROUP_OLDER_WEEK -> R.string.older_last_week - NoteTimeGroup.GROUP_OLDER_MONTH -> R.string.older_month - } - ) + // Display time group header + timeGroupHeaderText(note, previousNote)?.let { timeGroupText -> + with(noteViewHolder.headerText) { + visibility = View.VISIBLE + setText(timeGroupText) } + } ?: run { + noteViewHolder.headerText.visibility = View.GONE } // Subject is stored in db as html to preserve text formatting @@ -204,6 +212,7 @@ class NotesAdapter( noteViewHolder.textDetail.visibility = View.GONE } noteViewHolder.loadAvatars(note) + noteViewHolder.bindInlineActionIconsForNote(note) noteViewHolder.unreadNotificationView.isVisible = note.isUnread // request to load more comments when we near the end @@ -251,11 +260,37 @@ class NotesAdapter( private fun loadAvatar(imageView: ImageView, avatarUrl: String) { val url = GravatarUtils.fixGravatarUrl(avatarUrl, avatarSize) - imageManager?.loadIntoCircle(imageView, ImageType.AVATAR_WITH_BACKGROUND, url) + imageManager.loadIntoCircle(imageView, ImageType.AVATAR_WITH_BACKGROUND, url) } private fun Note.shouldShowMultipleAvatars() = isFollowType || isLikeType || isCommentLikeType + @Suppress("ForbiddenComment") + private fun NoteViewHolder.bindInlineActionIconsForNote(note: Note) { + when (note.type) { + NoteType.Comment -> { + actionIcon.setImageResource(R.drawable.star_empty) + actionIcon.isVisible = true + actionIcon.setOnClickListener { + // TODO: handle tap on comment's inline action icon (the star) + } + } + NoteType.NewPost, + NoteType.Reblog, + NoteType.Like -> { + // TODO: Use the icon from the Figma design + actionIcon.setImageResource(R.drawable.gb_ic_share) + actionIcon.isVisible = true + actionIcon.setOnClickListener { + // TODO: handle tap on comment's inline action icon (the share icon) + } + } + else -> { + actionIcon.isVisible = false + } + } + } + private fun handleMaxLines(subject: TextView, detail: TextView) { subject.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { @@ -315,6 +350,7 @@ class NotesAdapter( val threeAvatars2: ImageView val threeAvatars3: ImageView val unreadNotificationView: View + val actionIcon: ImageView init { contentView = checkNotNull(view.findViewById(R.id.note_content_container)) @@ -331,6 +367,7 @@ class NotesAdapter( twoAvatarsView = checkNotNull(view.findViewById(R.id.two_avatars_view)) threeAvatarsView = checkNotNull(view.findViewById(R.id.three_avatars_view)) unreadNotificationView = checkNotNull(view.findViewById(R.id.notification_unread)) + actionIcon = checkNotNull(view.findViewById(R.id.action)) contentView.setOnClickListener(onClickListener) } }