The reply button now works, and mentions are highlighted in compose mode.
This commit is contained in:
parent
2106d7a53c
commit
eddc15fdca
@ -26,7 +26,10 @@ import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.view.View;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Button;
|
||||
@ -54,19 +57,25 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ComposeActivity extends AppCompatActivity {
|
||||
private static final int STATUS_CHARACTER_LIMIT = 500;
|
||||
private static final int STATUS_MEDIA_SIZE_LIMIT = 4000000; // 4MB
|
||||
private static final int MEDIA_PICK_RESULT = 1;
|
||||
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
|
||||
private static final Pattern mentionPattern = Pattern.compile("\\B@[^\\s@]+@?[^\\s@]+");
|
||||
|
||||
private String inReplyToId;
|
||||
private String domain;
|
||||
private String accessToken;
|
||||
private EditText textEditor;
|
||||
@ -148,113 +157,58 @@ public class ComposeActivity extends AppCompatActivity {
|
||||
Snackbar.make(findViewById(R.id.activity_compose), stringId, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void onSendSuccess() {
|
||||
Toast.makeText(this, "Toot!", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
private static class Interval {
|
||||
public int start;
|
||||
public int end;
|
||||
}
|
||||
|
||||
private void onSendFailure(Exception exception) {
|
||||
textEditor.setError(getString(R.string.error_sending_status));
|
||||
}
|
||||
|
||||
private void sendStatus(String content, String visibility, boolean sensitive) {
|
||||
String endpoint = getString(R.string.endpoint_status);
|
||||
String url = "https://" + domain + endpoint;
|
||||
JSONObject parameters = new JSONObject();
|
||||
try {
|
||||
parameters.put("status", content);
|
||||
parameters.put("visibility", visibility);
|
||||
parameters.put("sensitive", sensitive);
|
||||
JSONArray media_ids = new JSONArray();
|
||||
for (QueuedMedia item : mediaQueued) {
|
||||
media_ids.put(item.getId());
|
||||
}
|
||||
if (media_ids.length() > 0) {
|
||||
parameters.put("media_ids", media_ids);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
onSendFailure(e);
|
||||
return;
|
||||
private static void colourMentions(Spannable text, int colour) {
|
||||
// Strip all existing colour spans.
|
||||
int n = text.length();
|
||||
ForegroundColorSpan[] oldSpans = text.getSpans(0, n, ForegroundColorSpan.class);
|
||||
for (int i = oldSpans.length - 1; i >= 0; i--) {
|
||||
text.removeSpan(oldSpans[i]);
|
||||
}
|
||||
JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, parameters,
|
||||
new Response.Listener<JSONObject>() {
|
||||
@Override
|
||||
public void onResponse(JSONObject response) {
|
||||
onSendSuccess();
|
||||
}
|
||||
}, new Response.ErrorListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
onSendFailure(error);
|
||||
}
|
||||
}) {
|
||||
// Match a list of new colour spans.
|
||||
List<Interval> intervals = new ArrayList<>();
|
||||
Matcher matcher = mentionPattern.matcher(text);
|
||||
while (matcher.find()) {
|
||||
Interval interval = new Interval();
|
||||
interval.start = matcher.start();
|
||||
interval.end = matcher.end();
|
||||
intervals.add(interval);
|
||||
}
|
||||
// Make sure intervals don't overlap.
|
||||
Collections.sort(intervals, new Comparator<Interval>() {
|
||||
@Override
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Authorization", "Bearer " + accessToken);
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
||||
}
|
||||
|
||||
private void readyStatus(final String content, final String visibility,
|
||||
final boolean sensitive) {
|
||||
final ProgressDialog dialog = ProgressDialog.show(this, "Finishing Media Upload",
|
||||
"Uploading...", true, true);
|
||||
final AsyncTask<Void, Void, Boolean> waitForMediaTask =
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
private Exception exception;
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
waitForMediaLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
exception = e;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean successful) {
|
||||
super.onPostExecute(successful);
|
||||
dialog.dismiss();
|
||||
if (successful) {
|
||||
sendStatus(content, visibility, sensitive);
|
||||
} else {
|
||||
onReadyFailure(exception, content, visibility, sensitive);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled() {
|
||||
removeAllMediaFromQueue();
|
||||
super.onCancelled();
|
||||
}
|
||||
};
|
||||
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
/* Generating an interrupt by passing true here is important because an interrupt
|
||||
* exception is the only thing that will kick the latch out of its waiting loop
|
||||
* early. */
|
||||
waitForMediaTask.cancel(true);
|
||||
public int compare(Interval a, Interval b) {
|
||||
return a.start - b.start;
|
||||
}
|
||||
});
|
||||
waitForMediaTask.execute();
|
||||
}
|
||||
|
||||
private void onReadyFailure(Exception exception, final String content, final String visibility,
|
||||
final boolean sensitive) {
|
||||
doErrorDialog(R.string.error_media_upload_sending, R.string.action_retry,
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
readyStatus(content, visibility, sensitive);
|
||||
for (int i = 0, j = 0; i < intervals.size() - 1; i++, j++) {
|
||||
if (j != 0) {
|
||||
Interval a = intervals.get(j - 1);
|
||||
Interval b = intervals.get(i);
|
||||
if (a.start <= b.end) {
|
||||
while (j != 0 && a.start <= b.end) {
|
||||
a = intervals.get(j - 1);
|
||||
b = intervals.get(i);
|
||||
a.end = Math.max(a.end, b.end);
|
||||
a.start = Math.min(a.start, b.start);
|
||||
j--;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
intervals.set(j, b);
|
||||
}
|
||||
} else {
|
||||
intervals.set(j, intervals.get(i));
|
||||
}
|
||||
}
|
||||
// Finally, set the spans.
|
||||
for (Interval interval : intervals) {
|
||||
text.setSpan(new ForegroundColorSpan(colour), interval.start, interval.end,
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -262,6 +216,13 @@ public class ComposeActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_compose);
|
||||
|
||||
Intent intent = getIntent();
|
||||
String[] mentionedUsernames = null;
|
||||
if (intent != null) {
|
||||
inReplyToId = intent.getStringExtra("in_reply_to_id");
|
||||
mentionedUsernames = intent.getStringArrayExtra("mentioned_usernames");
|
||||
}
|
||||
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
domain = preferences.getString("domain", null);
|
||||
@ -271,6 +232,7 @@ public class ComposeActivity extends AppCompatActivity {
|
||||
|
||||
textEditor = (EditText) findViewById(R.id.field_status);
|
||||
final TextView charactersLeft = (TextView) findViewById(R.id.characters_left);
|
||||
final int mentionColour = ContextCompat.getColor(this, R.color.compose_mention);
|
||||
TextWatcher textEditorWatcher = new TextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
@ -282,10 +244,23 @@ public class ComposeActivity extends AppCompatActivity {
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
public void afterTextChanged(Editable editable) {
|
||||
colourMentions(editable, mentionColour);
|
||||
}
|
||||
};
|
||||
textEditor.addTextChangedListener(textEditorWatcher);
|
||||
|
||||
if (mentionedUsernames != null) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String name : mentionedUsernames) {
|
||||
builder.append('@');
|
||||
builder.append(name);
|
||||
builder.append(' ');
|
||||
}
|
||||
textEditor.setText(builder);
|
||||
textEditor.setSelection(textEditor.length());
|
||||
}
|
||||
|
||||
mediaPreviewBar = (LinearLayout) findViewById(R.id.compose_media_preview_bar);
|
||||
mediaQueued = new ArrayList<>();
|
||||
waitForMediaLatch = new CountUpDownLatch();
|
||||
@ -332,6 +307,118 @@ public class ComposeActivity extends AppCompatActivity {
|
||||
markSensitive.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void onSendSuccess() {
|
||||
Toast.makeText(this, "Toot!", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
private void onSendFailure(Exception exception) {
|
||||
textEditor.setError(getString(R.string.error_sending_status));
|
||||
}
|
||||
|
||||
private void sendStatus(String content, String visibility, boolean sensitive) {
|
||||
String endpoint = getString(R.string.endpoint_status);
|
||||
String url = "https://" + domain + endpoint;
|
||||
JSONObject parameters = new JSONObject();
|
||||
try {
|
||||
parameters.put("status", content);
|
||||
parameters.put("visibility", visibility);
|
||||
parameters.put("sensitive", sensitive);
|
||||
if (inReplyToId != null) {
|
||||
parameters.put("in_reply_to_id", inReplyToId);
|
||||
}
|
||||
JSONArray media_ids = new JSONArray();
|
||||
for (QueuedMedia item : mediaQueued) {
|
||||
media_ids.put(item.getId());
|
||||
}
|
||||
if (media_ids.length() > 0) {
|
||||
parameters.put("media_ids", media_ids);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
onSendFailure(e);
|
||||
return;
|
||||
}
|
||||
JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, parameters,
|
||||
new Response.Listener<JSONObject>() {
|
||||
@Override
|
||||
public void onResponse(JSONObject response) {
|
||||
onSendSuccess();
|
||||
}
|
||||
}, new Response.ErrorListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
onSendFailure(error);
|
||||
}
|
||||
}) {
|
||||
@Override
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Authorization", "Bearer " + accessToken);
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
VolleySingleton.getInstance(this).addToRequestQueue(request);
|
||||
}
|
||||
|
||||
private void readyStatus(final String content, final String visibility,
|
||||
final boolean sensitive) {
|
||||
final ProgressDialog dialog = ProgressDialog.show(this, "Finishing Media Upload",
|
||||
"Uploading...", true, true);
|
||||
final AsyncTask<Void, Void, Boolean> waitForMediaTask =
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
private Exception exception;
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
waitForMediaLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
exception = e;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean successful) {
|
||||
super.onPostExecute(successful);
|
||||
dialog.dismiss();
|
||||
if (successful) {
|
||||
sendStatus(content, visibility, sensitive);
|
||||
} else {
|
||||
onReadyFailure(exception, content, visibility, sensitive);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled() {
|
||||
removeAllMediaFromQueue();
|
||||
super.onCancelled();
|
||||
}
|
||||
};
|
||||
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
/* Generating an interrupt by passing true here is important because an interrupt
|
||||
* exception is the only thing that will kick the latch out of its waiting loop
|
||||
* early. */
|
||||
waitForMediaTask.cancel(true);
|
||||
}
|
||||
});
|
||||
waitForMediaTask.execute();
|
||||
}
|
||||
|
||||
private void onReadyFailure(Exception exception, final String content, final String visibility,
|
||||
final boolean sensitive) {
|
||||
doErrorDialog(R.string.error_media_upload_sending, R.string.action_retry,
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
readyStatus(content, visibility, sensitive);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onMediaPick() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
@ -468,11 +555,10 @@ public class ComposeActivity extends AppCompatActivity {
|
||||
|
||||
private void downsizeMedia(final QueuedMedia item) {
|
||||
item.setReadyStage(QueuedMedia.ReadyStage.DOWNSIZING);
|
||||
InputStream stream = null;
|
||||
InputStream stream;
|
||||
try {
|
||||
stream = getContentResolver().openInputStream(item.getUri());
|
||||
} catch (FileNotFoundException e) {
|
||||
IOUtils.closeQuietly(stream);
|
||||
onMediaDownsizeFailure(item);
|
||||
return;
|
||||
}
|
||||
@ -567,11 +653,10 @@ public class ComposeActivity extends AppCompatActivity {
|
||||
public DataItem getData() {
|
||||
byte[] content = item.getContent();
|
||||
if (content == null) {
|
||||
InputStream stream = null;
|
||||
InputStream stream;
|
||||
try {
|
||||
stream = getContentResolver().openInputStream(item.getUri());
|
||||
} catch (FileNotFoundException e) {
|
||||
IOUtils.closeQuietly(stream);
|
||||
return null;
|
||||
}
|
||||
content = inputStreamGetBytes(stream);
|
||||
@ -638,11 +723,10 @@ public class ComposeActivity extends AppCompatActivity {
|
||||
break;
|
||||
}
|
||||
case "image": {
|
||||
InputStream stream = null;
|
||||
InputStream stream;
|
||||
try {
|
||||
stream = contentResolver.openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
IOUtils.closeQuietly(stream);
|
||||
displayTransientError(R.string.error_media_upload_opening);
|
||||
return;
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ public class Status {
|
||||
private boolean favourited;
|
||||
private Visibility visibility;
|
||||
private MediaAttachment[] attachments;
|
||||
private Mention[] mentions;
|
||||
private boolean sensitive;
|
||||
|
||||
public static final int MAX_MEDIA_ATTACHMENTS = 4;
|
||||
@ -61,6 +62,7 @@ public class Status {
|
||||
this.favourited = favourited;
|
||||
this.visibility = Visibility.valueOf(visibility.toUpperCase());
|
||||
this.attachments = new MediaAttachment[0];
|
||||
this.mentions = new Mention[0];
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
@ -111,6 +113,10 @@ public class Status {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public Mention[] getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public boolean getSensitive() {
|
||||
return sensitive;
|
||||
}
|
||||
@ -127,6 +133,10 @@ public class Status {
|
||||
this.favourited = favourited;
|
||||
}
|
||||
|
||||
public void setMentions(Mention[] mentions) {
|
||||
this.mentions = mentions;
|
||||
}
|
||||
|
||||
public void setAttachments(MediaAttachment[] attachments, boolean sensitive) {
|
||||
this.attachments = attachments;
|
||||
this.sensitive = sensitive;
|
||||
@ -196,6 +206,20 @@ public class Status {
|
||||
String username = account.getString("acct");
|
||||
String avatar = account.getString("avatar");
|
||||
|
||||
JSONArray mentionsArray = object.getJSONArray("mentions");
|
||||
Mention[] mentions = null;
|
||||
if (mentionsArray != null) {
|
||||
int n = mentionsArray.length();
|
||||
mentions = new Mention[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
JSONObject mention = mentionsArray.getJSONObject(i);
|
||||
String url = mention.getString("url");
|
||||
String mentionedUsername = mention.getString("acct");
|
||||
String mentionedAccountId = mention.getString("id");
|
||||
mentions[i] = new Mention(url, mentionedUsername, mentionedAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray mediaAttachments = object.getJSONArray("media_attachments");
|
||||
MediaAttachment[] attachments = null;
|
||||
if (mediaAttachments != null) {
|
||||
@ -230,9 +254,12 @@ public class Status {
|
||||
status = new Status(
|
||||
id, accountId, displayName, username, contentPlus, avatar, createdAt,
|
||||
reblogged, favourited, visibility);
|
||||
}
|
||||
if (attachments != null) {
|
||||
status.setAttachments(attachments, sensitive);
|
||||
if (mentions != null) {
|
||||
status.setMentions(mentions);
|
||||
}
|
||||
if (attachments != null) {
|
||||
status.setAttachments(attachments, sensitive);
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
@ -274,4 +301,28 @@ public class Status {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Mention {
|
||||
private String url;
|
||||
private String username;
|
||||
private String id;
|
||||
|
||||
public Mention(String url, String username, String id) {
|
||||
this.url = url;
|
||||
this.username = username;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package com.keylesspalace.tusky;
|
||||
import android.view.View;
|
||||
|
||||
public interface StatusActionListener {
|
||||
void onReply(int position);
|
||||
void onReblog(final boolean reblog, final int position);
|
||||
void onFavourite(final boolean favourite, final int position);
|
||||
void onMore(View view, final int position);
|
||||
|
@ -347,6 +347,12 @@ public class TimelineAdapter extends RecyclerView.Adapter {
|
||||
}
|
||||
|
||||
public void setupButtons(final StatusActionListener listener, final int position) {
|
||||
replyButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onReply(position);
|
||||
}
|
||||
});
|
||||
reblogButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
@ -31,6 +31,7 @@ import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -46,7 +47,10 @@ public class TimelineFragment extends Fragment implements
|
||||
|
||||
private String domain = null;
|
||||
private String accessToken = null;
|
||||
/** ID of the account that is currently logged-in. */
|
||||
private String userAccountId = null;
|
||||
/** Username of the account that is currently logged-in. */
|
||||
private String userUsername = null;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private RecyclerView recyclerView;
|
||||
private TimelineAdapter adapter;
|
||||
@ -149,6 +153,7 @@ public class TimelineFragment extends Fragment implements
|
||||
public void onResponse(JSONObject response) {
|
||||
try {
|
||||
userAccountId = response.getString("id");
|
||||
userUsername = response.getString("acct");
|
||||
} catch (JSONException e) {
|
||||
//TODO: Help
|
||||
assert(false);
|
||||
@ -273,6 +278,22 @@ public class TimelineFragment extends Fragment implements
|
||||
sendRequest(Request.Method.POST, endpoint, null, null);
|
||||
}
|
||||
|
||||
public void onReply(int position) {
|
||||
Status status = adapter.getItem(position);
|
||||
String inReplyToId = status.getId();
|
||||
Status.Mention[] mentions = status.getMentions();
|
||||
List<String> mentionedUsernames = new ArrayList<>();
|
||||
for (int i = 0; i < mentions.length; i++) {
|
||||
mentionedUsernames.add(mentions[i].getUsername());
|
||||
}
|
||||
mentionedUsernames.add(status.getUsername());
|
||||
mentionedUsernames.remove(userUsername);
|
||||
Intent intent = new Intent(getContext(), ComposeActivity.class);
|
||||
intent.putExtra("in_reply_to_id", inReplyToId);
|
||||
intent.putExtra("mentioned_usernames", mentionedUsernames.toArray(new String[0]));
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void onReblog(final boolean reblog, final int position) {
|
||||
final Status status = adapter.getItem(position);
|
||||
String id = status.getId();
|
||||
|
@ -7,4 +7,5 @@
|
||||
<color name="view_video_background">#000000</color>
|
||||
<color name="sensitive_media_warning_background">#303030</color>
|
||||
<color name="media_preview_unloaded_background">#DFDFDF</color>
|
||||
<color name="compose_mention">#4F5F6F</color>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user