diff options
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/models/Note.java')
-rw-r--r-- | WordPress/src/main/java/org/wordpress/android/models/Note.java | 590 |
1 files changed, 590 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/models/Note.java b/WordPress/src/main/java/org/wordpress/android/models/Note.java new file mode 100644 index 000000000..7dc1a463b --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/models/Note.java @@ -0,0 +1,590 @@ +/** + * Note represents a single WordPress.com notification + */ +package org.wordpress.android.models; + +import android.text.Html; +import android.text.Spannable; +import android.text.TextUtils; +import android.util.Log; + +import com.simperium.client.BucketSchema; +import com.simperium.client.Syncable; +import com.simperium.util.JSONDiff; + +import org.apache.commons.lang.time.DateUtils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.wordpress.android.ui.notifications.utils.NotificationsUtils; +import org.wordpress.android.util.DateTimeUtils; +import org.wordpress.android.util.JSONUtils; +import org.wordpress.android.util.StringUtils; + +import java.util.ArrayList; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; + +public class Note extends Syncable { + private static final String TAG = "NoteModel"; + + // Maximum character length for a comment preview + static private final int MAX_COMMENT_PREVIEW_LENGTH = 200; + + // Note types + public static final String NOTE_FOLLOW_TYPE = "follow"; + public static final String NOTE_LIKE_TYPE = "like"; + public static final String NOTE_COMMENT_TYPE = "comment"; + private static final String NOTE_MATCHER_TYPE = "automattcher"; + private static final String NOTE_COMMENT_LIKE_TYPE = "comment_like"; + private static final String NOTE_REBLOG_TYPE = "reblog"; + private static final String NOTE_UNKNOWN_TYPE = "unknown"; + + // JSON action keys + private static final String ACTION_KEY_REPLY = "replyto-comment"; + private static final String ACTION_KEY_APPROVE = "approve-comment"; + private static final String ACTION_KEY_SPAM = "spam-comment"; + private static final String ACTION_KEY_LIKE = "like-comment"; + + private JSONObject mActions; + private JSONObject mNoteJSON; + private final String mKey; + + private final Object mSyncLock = new Object(); + private String mLocalStatus; + + public enum EnabledActions { + ACTION_REPLY, + ACTION_APPROVE, + ACTION_UNAPPROVE, + ACTION_SPAM, + ACTION_LIKE + } + + public enum NoteTimeGroup { + GROUP_TODAY, + GROUP_YESTERDAY, + GROUP_OLDER_TWO_DAYS, + GROUP_OLDER_WEEK, + GROUP_OLDER_MONTH + } + + /** + * Create a note using JSON from Simperium + */ + private Note(String key, JSONObject noteJSON) { + mKey = key; + mNoteJSON = noteJSON; + } + + /** + * Simperium method @see Diffable + */ + @Override + public JSONObject getDiffableValue() { + synchronized (mSyncLock) { + return JSONDiff.deepCopy(mNoteJSON); + } + } + + /** + * Simperium method for identifying bucket object @see Diffable + */ + @Override + public String getSimperiumKey() { + return getId(); + } + + public String getId() { + return mKey; + } + + public String getType() { + return queryJSON("type", NOTE_UNKNOWN_TYPE); + } + + private Boolean isType(String type) { + return getType().equals(type); + } + + public Boolean isCommentType() { + synchronized (mSyncLock) { + return (isAutomattcherType() && JSONUtils.queryJSON(mNoteJSON, "meta.ids.comment", -1) != -1) || + isType(NOTE_COMMENT_TYPE); + } + } + + public Boolean isAutomattcherType() { + return isType(NOTE_MATCHER_TYPE); + } + + public Boolean isFollowType() { + return isType(NOTE_FOLLOW_TYPE); + } + + public Boolean isLikeType() { + return isType(NOTE_LIKE_TYPE); + } + + public Boolean isCommentLikeType() { + return isType(NOTE_COMMENT_LIKE_TYPE); + } + + public Boolean isReblogType() { + return isType(NOTE_REBLOG_TYPE); + } + + public Boolean isCommentReplyType() { + return isCommentType() && getParentCommentId() > 0; + } + + // Returns true if the user has replied to this comment note + public Boolean isCommentWithUserReply() { + return isCommentType() && !TextUtils.isEmpty(getCommentSubjectNoticon()); + } + + public Boolean isUserList() { + return isLikeType() || isCommentLikeType() || isFollowType() || isReblogType(); + } + + /* + * does user have permission to moderate/reply/spam this comment? + */ + public boolean canModerate() { + EnumSet<EnabledActions> enabledActions = getEnabledActions(); + return enabledActions != null && (enabledActions.contains(EnabledActions.ACTION_APPROVE) || enabledActions.contains(EnabledActions.ACTION_UNAPPROVE)); + } + + public boolean canMarkAsSpam() { + EnumSet<EnabledActions> enabledActions = getEnabledActions(); + return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_SPAM)); + } + + public boolean canReply() { + EnumSet<EnabledActions> enabledActions = getEnabledActions(); + return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_REPLY)); + } + + public boolean canTrash() { + return canModerate(); + } + + public boolean canEdit(int localBlogId) { + return (localBlogId > 0 && canModerate()); + } + + public boolean canLike() { + EnumSet<EnabledActions> enabledActions = getEnabledActions(); + return (enabledActions != null && enabledActions.contains(EnabledActions.ACTION_LIKE)); + } + + private String getLocalStatus() { + return StringUtils.notNullStr(mLocalStatus); + } + + public void setLocalStatus(String localStatus) { + mLocalStatus = localStatus; + } + + private JSONObject getSubject() { + try { + synchronized (mSyncLock) { + JSONArray subjectArray = mNoteJSON.getJSONArray("subject"); + if (subjectArray.length() > 0) { + return subjectArray.getJSONObject(0); + } + } + } catch (JSONException e) { + return null; + } + + return null; + } + + private Spannable getFormattedSubject() { + return NotificationsUtils.getSpannableContentForRanges(getSubject()); + } + + public String getTitle() { + return queryJSON("title", ""); + } + + private String getIconURL() { + return queryJSON("icon", ""); + } + + private String getCommentSubject() { + synchronized (mSyncLock) { + JSONArray subjectArray = mNoteJSON.optJSONArray("subject"); + if (subjectArray != null) { + String commentSubject = JSONUtils.queryJSON(subjectArray, "subject[1].text", ""); + + // Trim down the comment preview if the comment text is too large. + if (commentSubject != null && commentSubject.length() > MAX_COMMENT_PREVIEW_LENGTH) { + commentSubject = commentSubject.substring(0, MAX_COMMENT_PREVIEW_LENGTH - 1); + } + + return commentSubject; + } + + } + + return ""; + } + + private String getCommentSubjectNoticon() { + JSONArray subjectRanges = queryJSON("subject[0].ranges", new JSONArray()); + if (subjectRanges != null) { + for (int i=0; i < subjectRanges.length(); i++) { + try { + JSONObject rangeItem = subjectRanges.getJSONObject(i); + if (rangeItem.has("type") && rangeItem.optString("type").equals("noticon")) { + return rangeItem.optString("value", ""); + } + } catch (JSONException e) { + return ""; + } + } + } + + return ""; + } + + public long getCommentReplyId() { + return queryJSON("meta.ids.reply_comment", 0); + } + + /** + * Compare note timestamp to now and return a time grouping + */ + public static NoteTimeGroup getTimeGroupForTimestamp(long timestamp) { + Date today = new Date(); + Date then = new Date(timestamp * 1000); + + if (then.compareTo(DateUtils.addMonths(today, -1)) < 0) { + return NoteTimeGroup.GROUP_OLDER_MONTH; + } else if (then.compareTo(DateUtils.addWeeks(today, -1)) < 0) { + return NoteTimeGroup.GROUP_OLDER_WEEK; + } else if (then.compareTo(DateUtils.addDays(today, -2)) < 0 + || DateUtils.isSameDay(DateUtils.addDays(today, -2), then)) { + return NoteTimeGroup.GROUP_OLDER_TWO_DAYS; + } else if (DateUtils.isSameDay(DateUtils.addDays(today, -1), then)) { + return NoteTimeGroup.GROUP_YESTERDAY; + } else { + return NoteTimeGroup.GROUP_TODAY; + } + } + + /** + * The inverse of isRead + */ + public Boolean isUnread() { + return !isRead(); + } + + private Boolean isRead() { + return queryJSON("read", 0) == 1; + } + + public void markAsRead() { + try { + synchronized (mSyncLock) { + mNoteJSON.put("read", 1); + } + } catch (JSONException e) { + Log.e(TAG, "Unable to update note read property", e); + return; + } + save(); + } + + /** + * Get the timestamp provided by the API for the note + */ + public long getTimestamp() { + return DateTimeUtils.timestampFromIso8601(queryJSON("timestamp", "")); + } + + public JSONArray getBody() { + try { + synchronized (mSyncLock) { + return mNoteJSON.getJSONArray("body"); + } + } catch (JSONException e) { + return new JSONArray(); + } + } + + // returns character code for notification font + private String getNoticonCharacter() { + return queryJSON("noticon", ""); + } + + private JSONObject getCommentActions() { + if (mActions == null) { + // Find comment block that matches the root note comment id + long commentId = getCommentId(); + JSONArray bodyArray = getBody(); + for (int i = 0; i < bodyArray.length(); i++) { + try { + JSONObject bodyItem = bodyArray.getJSONObject(i); + if (bodyItem.has("type") && bodyItem.optString("type").equals("comment") + && commentId == JSONUtils.queryJSON(bodyItem, "meta.ids.comment", 0)) { + mActions = JSONUtils.queryJSON(bodyItem, "actions", new JSONObject()); + break; + } + } catch (JSONException e) { + break; + } + } + + if (mActions == null) { + mActions = new JSONObject(); + } + } + + return mActions; + } + + + private void updateJSON(JSONObject json) { + synchronized (mSyncLock) { + mNoteJSON = json; + } + } + + /* + * returns the actions allowed on this note, assumes it's a comment notification + */ + public EnumSet<EnabledActions> getEnabledActions() { + EnumSet<EnabledActions> actions = EnumSet.noneOf(EnabledActions.class); + JSONObject jsonActions = getCommentActions(); + if (jsonActions == null || jsonActions.length() == 0) { + return actions; + } + + if (jsonActions.has(ACTION_KEY_REPLY)) { + actions.add(EnabledActions.ACTION_REPLY); + } + if (jsonActions.has(ACTION_KEY_APPROVE) && jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) { + actions.add(EnabledActions.ACTION_UNAPPROVE); + } + if (jsonActions.has(ACTION_KEY_APPROVE) && !jsonActions.optBoolean(ACTION_KEY_APPROVE, false)) { + actions.add(EnabledActions.ACTION_APPROVE); + } + if (jsonActions.has(ACTION_KEY_SPAM)) { + actions.add(EnabledActions.ACTION_SPAM); + } + if (jsonActions.has(ACTION_KEY_LIKE)) { + actions.add(EnabledActions.ACTION_LIKE); + } + + return actions; + } + + public int getSiteId() { + return queryJSON("meta.ids.site", 0); + } + + public int getPostId() { + return queryJSON("meta.ids.post", 0); + } + + public long getCommentId() { + return queryJSON("meta.ids.comment", 0); + } + + public long getParentCommentId() { + return queryJSON("meta.ids.parent_comment", 0); + } + + /** + * Rudimentary system for pulling an item out of a JSON object hierarchy + */ + private <U> U queryJSON(String query, U defaultObject) { + synchronized (mSyncLock) { + if (mNoteJSON == null) return defaultObject; + return JSONUtils.queryJSON(mNoteJSON, query, defaultObject); + } + } + + /** + * Constructs a new Comment object based off of data in a Note + */ + public Comment buildComment() { + return new Comment( + getPostId(), + getCommentId(), + getCommentAuthorName(), + DateTimeUtils.iso8601FromTimestamp(getTimestamp()), + getCommentText(), + CommentStatus.toString(getCommentStatus()), + "", // post title is unavailable in note model + getCommentAuthorUrl(), + "", // user email is unavailable in note model + getIconURL() + ); + } + + public String getCommentAuthorName() { + JSONArray bodyArray = getBody(); + + for (int i=0; i < bodyArray.length(); i++) { + try { + JSONObject bodyItem = bodyArray.getJSONObject(i); + if (bodyItem.has("type") && bodyItem.optString("type").equals("user")) { + return bodyItem.optString("text"); + } + } catch (JSONException e) { + return ""; + } + } + + return ""; + } + + private String getCommentText() { + return queryJSON("body[last].text", ""); + } + + private String getCommentAuthorUrl() { + JSONArray bodyArray = getBody(); + + for (int i=0; i < bodyArray.length(); i++) { + try { + JSONObject bodyItem = bodyArray.getJSONObject(i); + if (bodyItem.has("type") && bodyItem.optString("type").equals("user")) { + return JSONUtils.queryJSON(bodyItem, "meta.links.home", ""); + } + } catch (JSONException e) { + return ""; + } + } + + return ""; + } + + public CommentStatus getCommentStatus() { + EnumSet<EnabledActions> enabledActions = getEnabledActions(); + + if (enabledActions.contains(EnabledActions.ACTION_UNAPPROVE)) { + return CommentStatus.APPROVED; + } else if (enabledActions.contains(EnabledActions.ACTION_APPROVE)) { + return CommentStatus.UNAPPROVED; + } + + return CommentStatus.UNKNOWN; + } + + public boolean hasLikedComment() { + JSONObject jsonActions = getCommentActions(); + return !(jsonActions == null || jsonActions.length() == 0) && jsonActions.optBoolean(ACTION_KEY_LIKE); + } + + public String getUrl() { + return queryJSON("url", ""); + } + + public JSONArray getHeader() { + synchronized (mSyncLock) { + return mNoteJSON.optJSONArray("header"); + } + } + + /** + * Represents a user replying to a note. + */ + public static class Reply { + private final String mContent; + private final String mRestPath; + + Reply(String restPath, String content) { + mRestPath = restPath; + mContent = content; + } + + public String getContent() { + return mContent; + } + + public String getRestPath() { + return mRestPath; + } + } + + public Reply buildReply(String content) { + String restPath; + if (this.isCommentType()) { + restPath = String.format("sites/%d/comments/%d", getSiteId(), getCommentId()); + } else { + restPath = String.format("sites/%d/posts/%d", getSiteId(), getPostId()); + } + + return new Reply(String.format("%s/replies/new", restPath), content); + } + + /** + * Simperium Schema + */ + public static class Schema extends BucketSchema<Note> { + + static public final String NAME = "note20"; + static public final String TIMESTAMP_INDEX = "timestamp"; + static public final String SUBJECT_INDEX = "subject"; + static public final String SNIPPET_INDEX = "snippet"; + static public final String UNREAD_INDEX = "unread"; + static public final String NOTICON_INDEX = "noticon"; + static public final String ICON_URL_INDEX = "icon"; + static public final String IS_UNAPPROVED_INDEX = "unapproved"; + static public final String COMMENT_SUBJECT_NOTICON = "comment_subject_noticon"; + static public final String LOCAL_STATUS = "local_status"; + static public final String TYPE_INDEX = "type"; + + private static final Indexer<Note> sNoteIndexer = new Indexer<Note>() { + + @Override + public List<Index> index(Note note) { + List<Index> indexes = new ArrayList<>(); + try { + indexes.add(new Index(TIMESTAMP_INDEX, note.getTimestamp())); + } catch (NumberFormatException e) { + // note will not have an indexed timestamp so it will + // show up at the end of a query sorting by timestamp + android.util.Log.e("WordPress", "Failed to index timestamp", e); + } + + indexes.add(new Index(SUBJECT_INDEX, Html.toHtml(note.getFormattedSubject()))); + indexes.add(new Index(SNIPPET_INDEX, note.getCommentSubject())); + indexes.add(new Index(UNREAD_INDEX, note.isUnread())); + indexes.add(new Index(NOTICON_INDEX, note.getNoticonCharacter())); + indexes.add(new Index(ICON_URL_INDEX, note.getIconURL())); + indexes.add(new Index(IS_UNAPPROVED_INDEX, note.getCommentStatus() == CommentStatus.UNAPPROVED)); + indexes.add(new Index(COMMENT_SUBJECT_NOTICON, note.getCommentSubjectNoticon())); + indexes.add(new Index(LOCAL_STATUS, note.getLocalStatus())); + indexes.add(new Index(TYPE_INDEX, note.getType())); + + return indexes; + } + + }; + + public Schema() { + addIndex(sNoteIndexer); + } + + @Override + public String getRemoteName() { + return NAME; + } + + @Override + public Note build(String key, JSONObject properties) { + return new Note(key, properties); + } + + public void update(Note note, JSONObject properties) { + note.updateJSON(properties); + } + } +} |