added initial status history support

This commit is contained in:
nuclearfog 2023-10-22 18:18:01 +02:00
parent 2cf828bf10
commit 6124810047
No known key found for this signature in database
GPG Key ID: 03488A185C476379
15 changed files with 345 additions and 77 deletions

View File

@ -32,6 +32,7 @@ import org.nuclearfog.twidda.model.lists.Hashtags;
import org.nuclearfog.twidda.model.lists.Notifications;
import org.nuclearfog.twidda.model.lists.Rules;
import org.nuclearfog.twidda.model.lists.ScheduledStatuses;
import org.nuclearfog.twidda.model.lists.StatusEditHistoy;
import org.nuclearfog.twidda.model.lists.Statuses;
import org.nuclearfog.twidda.model.lists.UserLists;
import org.nuclearfog.twidda.model.lists.Users;
@ -283,6 +284,14 @@ public interface Connection {
*/
Statuses getPublicTimeline(long minId, long maxId) throws ConnectionException;
/**
* get an edit history of a status
*
* @param id ID of the status
* @return list of edited posts
*/
StatusEditHistoy getStatusEditHistory(long id) throws ConnectionException;
/**
* get trending hashtags
*

View File

@ -14,6 +14,7 @@ import org.nuclearfog.twidda.BuildConfig;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.api.Connection;
import org.nuclearfog.twidda.backend.api.ConnectionException;
import org.nuclearfog.twidda.backend.api.mastodon.impl.EditedMastodonStatus;
import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonAccount;
import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonCredentials;
import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonEmoji;
@ -64,6 +65,7 @@ import org.nuclearfog.twidda.model.lists.Hashtags;
import org.nuclearfog.twidda.model.lists.Notifications;
import org.nuclearfog.twidda.model.lists.Rules;
import org.nuclearfog.twidda.model.lists.ScheduledStatuses;
import org.nuclearfog.twidda.model.lists.StatusEditHistoy;
import org.nuclearfog.twidda.model.lists.Statuses;
import org.nuclearfog.twidda.model.lists.UserLists;
import org.nuclearfog.twidda.model.lists.Users;
@ -494,6 +496,26 @@ public class Mastodon implements Connection {
}
@Override
public StatusEditHistoy getStatusEditHistory(long id) throws ConnectionException {
try {
StatusEditHistoy result = new StatusEditHistoy();
Response response = get(ENDPOINT_STATUS + id + "/history", new ArrayList<>());
ResponseBody body = response.body();
if (response.code() == 200 && body != null) {
JSONArray array = new JSONArray(body.string());
long currentId = settings.getLogin().getId();
for (int i = 0; i < array.length(); i++) {
result.add(new EditedMastodonStatus(array.getJSONObject(i), currentId));
}
}
return result;
} catch (IOException | JSONException exception) {
throw new MastodonException(exception);
}
}
@Override
public Hashtags getHashtags() throws MastodonException {
Hashtags result = getHashtags(ENDPOINT_TRENDS, new ArrayList<>());

View File

@ -0,0 +1,115 @@
package org.nuclearfog.twidda.backend.api.mastodon.impl;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.nuclearfog.twidda.backend.utils.StringUtils;
import org.nuclearfog.twidda.model.EditedStatus;
import org.nuclearfog.twidda.model.Emoji;
import org.nuclearfog.twidda.model.Media;
import org.nuclearfog.twidda.model.Poll;
import org.nuclearfog.twidda.model.User;
/**
* Mastodon implementation of {@link EditedStatus}
*
* @author nuclearfog
*/
public class EditedMastodonStatus implements EditedStatus {
private static final long serialVersionUID = 7283516503545009772L;
private long timestamp;
private boolean sensitive;
private boolean spoiler;
private String text;
private User author;
private Poll poll;
private Media[] medias = {};
private Emoji[] emojis = {};
/**
*
*/
public EditedMastodonStatus(JSONObject json, long currentUserId) throws JSONException {
JSONObject pollJson = json.optJSONObject("poll");
JSONArray mediaArray = json.optJSONArray("media_attachments");
JSONArray emojiArray = json.optJSONArray("emojis");
String content = json.optString("content", "");
String spoilerText = json.optString("spoiler_text", "");
text = StringUtils.extractText(content);
spoiler = !content.equals(spoilerText);
sensitive = json.optBoolean("sensitive", false);
timestamp = StringUtils.getIsoTime(json.optString("created_at"));
author = new MastodonUser(json.getJSONObject("account"), currentUserId);
if (pollJson != null) {
poll = new MastodonPoll(pollJson);
}
if (mediaArray != null && mediaArray.length() > 0) {
medias = new Media[mediaArray.length()];
for (int i = 0; i < mediaArray.length(); i++) {
JSONObject mediaItem = mediaArray.getJSONObject(i);
medias[i] = new MastodonMedia(mediaItem);
}
}
if (emojiArray != null && emojiArray.length() > 0) {
emojis = new Emoji[emojiArray.length()];
for (int i = 0; i < emojis.length; i++) {
JSONObject emojiJson = emojiArray.getJSONObject(i);
emojis[i] = new MastodonEmoji(emojiJson);
}
}
}
@Override
public long getTimestamp() {
return timestamp;
}
@Override
public String getText() {
return text;
}
@Override
public User getAuthor() {
return author;
}
@Override
public boolean isSensitive() {
return sensitive;
}
@Override
public boolean isSpoiler() {
return spoiler;
}
@Nullable
@Override
public Poll getPoll() {
return poll;
}
@Override
public Media[] getMedia() {
return medias;
}
@Override
public Emoji[] getEmojis() {
return emojis;
}
}

View File

@ -30,6 +30,7 @@ public class MastodonStatus implements Status {
private long replyId;
private long replyUserId;
private long createdAt;
private long editedAt;
private int replyCount;
private int favoriteCount;
@ -72,6 +73,7 @@ public class MastodonStatus implements Status {
String idStr = json.getString("id");
String visibilityStr = json.getString("visibility");
String language = json.optString("language", "");
String editedAtStr = json.optString("edited_at");
author = new MastodonUser(json.getJSONObject("account"), currentUserId);
createdAt = StringUtils.getIsoTime(json.optString("created_at"));
@ -86,6 +88,8 @@ public class MastodonStatus implements Status {
bookmarked = json.optBoolean("bookmarked", false);
text = StringUtils.extractText(json.optString("content", ""));
if (!editedAtStr.isEmpty() && !editedAtStr.equals("null"))
editedAt = StringUtils.getIsoTime(editedAtStr);
if (author.getId() != currentUserId)
mentions = author.getScreenname() + ' ';
if (embeddedJson != null) {
@ -275,6 +279,12 @@ public class MastodonStatus implements Status {
}
@Override
public long editedAt() {
return editedAt;
}
@Override
public boolean isSensitive() {
return sensitive;

View File

@ -1442,7 +1442,7 @@ public class AppDatabase {
default:
flags &= ~MASK_STATUS_VISIBILITY_DIRECT;
}
ContentValues column = new ContentValues(20);
ContentValues column = new ContentValues(22);
column.put(StatusTable.ID, status.getId());
column.put(StatusTable.USER, user.getId());
column.put(StatusTable.TIME, status.getTimestamp());
@ -1458,6 +1458,7 @@ public class AppDatabase {
column.put(StatusTable.REPLYUSER, status.getRepliedUserId());
column.put(StatusTable.REPLYNAME, status.getReplyName());
column.put(StatusTable.LANGUAGE, status.getLanguage());
column.put(StatusTable.EDITED_AT, status.editedAt());
column.put(StatusTable.MENTIONS, status.getUserMentions());
if (status.getLocation() != null && status.getLocation().getId() != 0L) {
column.put(StatusTable.LOCATION, status.getLocation().getId());

View File

@ -20,7 +20,7 @@ public class DatabaseAdapter {
/**
* database version
*/
private static final int DB_VERSION = 21;
private static final int DB_VERSION = 22;
/**
* database file name
@ -70,7 +70,8 @@ public class DatabaseAdapter {
+ StatusTable.SOURCE + " TEXT,"
+ StatusTable.MENTIONS + " TEXT,"
+ StatusTable.LOCATION + " INTEGER,"
+ StatusTable.LANGUAGE + " TEXT);";
+ StatusTable.LANGUAGE + " TEXT,"
+ StatusTable.EDITED_AT + " INTEGER);";
/**
* SQL query to create a table for trend information
@ -312,6 +313,11 @@ public class DatabaseAdapter {
*/
private static final String UPDATE_HASHTAG_ADD_ID = "ALTER TABLE " + HashtagTable.NAME + " ADD " + HashtagTable.ID + " INTEGER;";
/**
* add status edit timestamp
*/
private static final String UPDATE_STATUS_ADD_EDIT_TIME = "ALTER TABLE " + StatusTable.NAME + " ADD " + StatusTable.EDITED_AT + " INTEGER;";
/**
* singleton instance
*/
@ -418,8 +424,12 @@ public class DatabaseAdapter {
db.execSQL(TABLE_EMOJI);
db.setVersion(20);
}
if (db.getVersion() < DB_VERSION) {
if (db.getVersion() < 21) {
db.execSQL(UPDATE_HASHTAG_ADD_ID);
db.setVersion(21);
}
if (db.getVersion() < DB_VERSION) {
db.execSQL(UPDATE_STATUS_ADD_EDIT_TIME);
db.setVersion(DB_VERSION);
}
}
@ -660,6 +670,11 @@ public class DatabaseAdapter {
* language of the status
*/
String LANGUAGE = "lang";
/**
* timestamp of the last edit
*/
String EDITED_AT = "edited_at";
}
/**

View File

@ -30,7 +30,7 @@ public class DatabaseStatus implements Status, StatusTable, StatusRegisterTable
private static final Pattern KEY_SEPARATOR = Pattern.compile(";");
private long id, time, embeddedId, replyID, replyUserId, myRepostId, locationId, pollId;
private long id, time, embeddedId, replyID, replyUserId, myRepostId, locationId, pollId, editedAt;
private int repostCount, favoriteCount, replyCount, visibility;
private boolean reposted, favorited, bookmarked, sensitive, spoiler, isHidden;
private Status embedded;
@ -66,6 +66,7 @@ public class DatabaseStatus implements Status, StatusTable, StatusRegisterTable
replyUserId = cursor.getLong(cursor.getColumnIndexOrThrow(REPLYUSER));
embeddedId = cursor.getLong(cursor.getColumnIndexOrThrow(EMBEDDED));
myRepostId = cursor.getLong(cursor.getColumnIndexOrThrow(REPOST_ID));
editedAt = cursor.getLong(cursor.getColumnIndexOrThrow(EDITED_AT));
String statusUrl = cursor.getString(cursor.getColumnIndexOrThrow(URL));
String language = cursor.getString(cursor.getColumnIndexOrThrow(LANGUAGE));
String mediaKeys = cursor.getString(cursor.getColumnIndexOrThrow(MEDIA));
@ -195,6 +196,12 @@ public class DatabaseStatus implements Status, StatusTable, StatusRegisterTable
}
@Override
public long editedAt() {
return editedAt;
}
@NonNull
@Override
public Media[] getMedia() {

View File

@ -241,7 +241,7 @@ public class DatabaseUser implements User, UserTable, UserRegisterTable {
}
/**
* setup
* setup account configuration
*/
public void setAccountInformation(Account account) {
isCurrentUser = true;

View File

@ -0,0 +1,54 @@
package org.nuclearfog.twidda.model;
import androidx.annotation.Nullable;
import java.io.Serializable;
/**
* Represents a revision of a {@link Status} that has been edited
*
* @author nuclearfog
*/
public interface EditedStatus extends Serializable {
/**
* @return timestamp of this revision
*/
long getTimestamp();
/**
* @return status text
*/
String getText();
/**
* @return author of this status
*/
User getAuthor();
/**
* @return true if status contains sensitive content
*/
boolean isSensitive();
/**
* @return true if status contains spoiler content
*/
boolean isSpoiler();
/**
* @return status poll or null
*/
@Nullable
Poll getPoll();
/**
* @return array of media items
*/
Media[] getMedia();
/**
* @return array of emojis used in the text
*/
Emoji[] getEmojis();
}

View File

@ -152,6 +152,11 @@ public interface Status extends Serializable, Comparable<Status> {
*/
boolean isHidden();
/**
* @return Timestamp of when the status was last edited or '0' if status is not edited
*/
long editedAt();
/**
* @return url of the status
*/

View File

@ -0,0 +1,13 @@
package org.nuclearfog.twidda.model.lists;
import org.nuclearfog.twidda.model.EditedStatus;
import java.util.LinkedList;
/**
* @author nuclearfog
*/
public class StatusEditHistoy extends LinkedList<EditedStatus> {
private static final long serialVersionUID = 6241896565923670373L;
}

View File

@ -371,6 +371,7 @@ public class StatusActivity extends AppCompatActivity implements OnClickListener
MenuItem optReport = m.findItem(R.id.menu_status_report);
MenuItem menuBookmark = m.findItem(R.id.menu_status_bookmark);
MenuItem editStatus = m.findItem(R.id.menu_status_edit);
MenuItem editHistory = m.findItem(R.id.menu_status_history);
SubMenu copyMenu = optCopy.getSubMenu();
// set status options
@ -400,6 +401,9 @@ public class StatusActivity extends AppCompatActivity implements OnClickListener
} else {
optReport.setVisible(true);
}
if (currentStatus.editedAt() != 0L) {
editHistory.setVisible(true);
}
// add media link items
// check if menu doesn't contain media links already
if (copyMenu.size() == 2) {
@ -411,88 +415,94 @@ public class StatusActivity extends AppCompatActivity implements OnClickListener
}
return true;
}
return super.onPrepareOptionsMenu(m);
return false;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (status == null)
return super.onOptionsItemSelected(item);
Status status = this.status;
if (status.getEmbeddedStatus() != null)
status = status.getEmbeddedStatus();
// Delete status option
if (item.getItemId() == R.id.menu_status_delete) {
confirmDialog.show(ConfirmDialog.DELETE_STATUS);
return true;
}
// add/remove bookmark
else if (item.getItemId() == R.id.menu_status_bookmark) {
Toast.makeText(getApplicationContext(), R.string.info_loading, Toast.LENGTH_SHORT).show();
int mode = status.isBookmarked() ? StatusAction.Param.UNBOOKMARK : StatusAction.Param.BOOKMARK;
StatusAction.Param param = new StatusAction.Param(mode, status.getId());
statusLoader.execute(param, statusCallback);
return true;
}
// hide status
else if (item.getItemId() == R.id.menu_status_hide) {
int mode = hidden ? StatusAction.Param.UNHIDE : StatusAction.Param.HIDE;
StatusAction.Param param = new StatusAction.Param(mode, status.getId());
statusLoader.execute(param, statusCallback);
return true;
}
// get status link
else if (item.getItemId() == R.id.menu_status_browser) {
if (!status.getUrl().isEmpty()) {
LinkUtils.openLink(this, status.getUrl());
if (status != null) {
Status status = this.status;
if (status.getEmbeddedStatus() != null)
status = status.getEmbeddedStatus();
// Delete status option
if (item.getItemId() == R.id.menu_status_delete) {
confirmDialog.show(ConfirmDialog.DELETE_STATUS);
return true;
}
return true;
}
// copy status link to clipboard
else if (item.getItemId() == R.id.menu_status_copy_text) {
if (clip != null) {
ClipData linkClip = ClipData.newPlainText("status text", status.getText());
clip.setPrimaryClip(linkClip);
Toast.makeText(getApplicationContext(), R.string.info_status_text_copied, Toast.LENGTH_SHORT).show();
// add/remove bookmark
else if (item.getItemId() == R.id.menu_status_bookmark) {
Toast.makeText(getApplicationContext(), R.string.info_loading, Toast.LENGTH_SHORT).show();
int mode = status.isBookmarked() ? StatusAction.Param.UNBOOKMARK : StatusAction.Param.BOOKMARK;
StatusAction.Param param = new StatusAction.Param(mode, status.getId());
statusLoader.execute(param, statusCallback);
return true;
}
return true;
}
// copy status link to clipboard
else if (item.getItemId() == R.id.menu_status_copy_link) {
if (clip != null) {
ClipData linkClip = ClipData.newPlainText("status link", status.getUrl());
clip.setPrimaryClip(linkClip);
Toast.makeText(getApplicationContext(), R.string.info_status_link_copied, Toast.LENGTH_SHORT).show();
// hide status
else if (item.getItemId() == R.id.menu_status_hide) {
int mode = hidden ? StatusAction.Param.UNHIDE : StatusAction.Param.HIDE;
StatusAction.Param param = new StatusAction.Param(mode, status.getId());
statusLoader.execute(param, statusCallback);
return true;
}
return true;
}
// copy media links
else if (item.getGroupId() == MENU_GROUP_COPY) {
int index = item.getItemId();
Media[] medias = status.getMedia();
if (index >= 0 && index < medias.length) {
if (clip != null) {
ClipData linkClip = ClipData.newPlainText("status media link", medias[index].getUrl());
clip.setPrimaryClip(linkClip);
Toast.makeText(getApplicationContext(), R.string.info_status_medialink_copied, Toast.LENGTH_SHORT).show();
// get status link
else if (item.getItemId() == R.id.menu_status_browser) {
if (!status.getUrl().isEmpty()) {
LinkUtils.openLink(this, status.getUrl());
}
return true;
}
// copy status link to clipboard
else if (item.getItemId() == R.id.menu_status_copy_text) {
if (clip != null) {
ClipData linkClip = ClipData.newPlainText("status text", status.getText());
clip.setPrimaryClip(linkClip);
Toast.makeText(getApplicationContext(), R.string.info_status_text_copied, Toast.LENGTH_SHORT).show();
}
return true;
}
// copy status link to clipboard
else if (item.getItemId() == R.id.menu_status_copy_link) {
if (clip != null) {
ClipData linkClip = ClipData.newPlainText("status link", status.getUrl());
clip.setPrimaryClip(linkClip);
Toast.makeText(getApplicationContext(), R.string.info_status_link_copied, Toast.LENGTH_SHORT).show();
}
return true;
}
// copy media links
else if (item.getGroupId() == MENU_GROUP_COPY) {
int index = item.getItemId();
Media[] medias = status.getMedia();
if (index >= 0 && index < medias.length) {
if (clip != null) {
ClipData linkClip = ClipData.newPlainText("status media link", medias[index].getUrl());
clip.setPrimaryClip(linkClip);
Toast.makeText(getApplicationContext(), R.string.info_status_medialink_copied, Toast.LENGTH_SHORT).show();
}
}
return true;
}
// edit status
else if (item.getItemId() == R.id.menu_status_edit) {
Intent intent = new Intent(this, StatusEditor.class);
intent.putExtra(StatusEditor.KEY_STATUS_DATA, status);
intent.putExtra(StatusEditor.KEY_EDIT, true);
activityResultLauncher.launch(intent);
return true;
}
// report status
else if (item.getItemId() == R.id.menu_status_report) {
reportDialog.show(status.getAuthor().getId(), status.getId());
return true;
}
// get edit history
else if (item.getItemId() == R.id.menu_status_history) {
// todo implement history viewer
return true;
}
return true;
}
// edit status
else if (item.getItemId() == R.id.menu_status_edit) {
Intent intent = new Intent(this, StatusEditor.class);
intent.putExtra(StatusEditor.KEY_STATUS_DATA, status);
intent.putExtra(StatusEditor.KEY_EDIT, true);
activityResultLauncher.launch(intent);
}
// report status
else if (item.getItemId() == R.id.menu_status_report) {
reportDialog.show(status.getAuthor().getId(), status.getId());
}
return super.onOptionsItemSelected(item);
return false;
}

View File

@ -34,6 +34,11 @@
android:title="@string/menu_status_edit"
android:visible="false" />
<item
android:id="@+id/menu_status_history"
android:title="@string/menu_status_history"
android:visible="false" />
<item
android:id="@+id/menu_status_delete"
android:title="@string/menu_status_delete"

View File

@ -364,4 +364,5 @@
<string name="info_follow_request_accepted">Follow-Anfrage angenommen</string>
<string name="description_show_timeline">öffentl. Timeline</string>
<string name="error_api_error">API Fehler</string>
<string name="menu_status_history">Bearbeitungsverlauf</string>
</resources>

View File

@ -152,6 +152,7 @@
<string name="menu_bookmark_remove">remove bookmark</string>
<string name="menu_unmute_user">unmute</string>
<string name="menu_status_edit">edit</string>
<string name="menu_status_history">history</string>
<string name="menu_status_delete">delete</string>
<string name="menu_status_report">report</string>
<string name="menu_status_hide">hide</string>