| /** |
| * 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); |
| } |
| } |
| } |