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,6 +157,156 @@ public class ComposeActivity extends AppCompatActivity {
|
|||
Snackbar.make(findViewById(R.id.activity_compose), stringId, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private static class Interval {
|
||||
public int start;
|
||||
public int end;
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
// 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 int compare(Interval a, Interval b) {
|
||||
return a.start - b.start;
|
||||
}
|
||||
});
|
||||
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
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
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);
|
||||
accessToken = preferences.getString("accessToken", null);
|
||||
assert(domain != null);
|
||||
assert(accessToken != null);
|
||||
|
||||
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) {
|
||||
int left = STATUS_CHARACTER_LIMIT - s.length();
|
||||
charactersLeft.setText(Integer.toString(left));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
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();
|
||||
|
||||
final RadioGroup radio = (RadioGroup) findViewById(R.id.radio_visibility);
|
||||
final Button sendButton = (Button) findViewById(R.id.button_send);
|
||||
sendButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Editable editable = textEditor.getText();
|
||||
if (editable.length() <= STATUS_CHARACTER_LIMIT) {
|
||||
int id = radio.getCheckedRadioButtonId();
|
||||
String visibility;
|
||||
switch (id) {
|
||||
default:
|
||||
case R.id.radio_public: {
|
||||
visibility = "public";
|
||||
break;
|
||||
}
|
||||
case R.id.radio_unlisted: {
|
||||
visibility = "unlisted";
|
||||
break;
|
||||
}
|
||||
case R.id.radio_private: {
|
||||
visibility = "private";
|
||||
break;
|
||||
}
|
||||
}
|
||||
readyStatus(editable.toString(), visibility, markSensitive.isChecked());
|
||||
} else {
|
||||
textEditor.setError(getString(R.string.error_compose_character_limit));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mediaPick = (ImageButton) findViewById(R.id.compose_photo_pick);
|
||||
mediaPick.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onMediaPick();
|
||||
}
|
||||
});
|
||||
markSensitive = (CheckBox) findViewById(R.id.compose_mark_sensitive);
|
||||
markSensitive.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void onSendSuccess() {
|
||||
Toast.makeText(this, "Toot!", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
|
@ -165,6 +324,9 @@ public class ComposeActivity extends AppCompatActivity {
|
|||
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());
|
||||
|
@ -257,81 +419,6 @@ public class ComposeActivity extends AppCompatActivity {
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_compose);
|
||||
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
domain = preferences.getString("domain", null);
|
||||
accessToken = preferences.getString("accessToken", null);
|
||||
assert(domain != null);
|
||||
assert(accessToken != null);
|
||||
|
||||
textEditor = (EditText) findViewById(R.id.field_status);
|
||||
final TextView charactersLeft = (TextView) findViewById(R.id.characters_left);
|
||||
TextWatcher textEditorWatcher = new TextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
int left = STATUS_CHARACTER_LIMIT - s.length();
|
||||
charactersLeft.setText(Integer.toString(left));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
};
|
||||
textEditor.addTextChangedListener(textEditorWatcher);
|
||||
|
||||
mediaPreviewBar = (LinearLayout) findViewById(R.id.compose_media_preview_bar);
|
||||
mediaQueued = new ArrayList<>();
|
||||
waitForMediaLatch = new CountUpDownLatch();
|
||||
|
||||
final RadioGroup radio = (RadioGroup) findViewById(R.id.radio_visibility);
|
||||
final Button sendButton = (Button) findViewById(R.id.button_send);
|
||||
sendButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Editable editable = textEditor.getText();
|
||||
if (editable.length() <= STATUS_CHARACTER_LIMIT) {
|
||||
int id = radio.getCheckedRadioButtonId();
|
||||
String visibility;
|
||||
switch (id) {
|
||||
default:
|
||||
case R.id.radio_public: {
|
||||
visibility = "public";
|
||||
break;
|
||||
}
|
||||
case R.id.radio_unlisted: {
|
||||
visibility = "unlisted";
|
||||
break;
|
||||
}
|
||||
case R.id.radio_private: {
|
||||
visibility = "private";
|
||||
break;
|
||||
}
|
||||
}
|
||||
readyStatus(editable.toString(), visibility, markSensitive.isChecked());
|
||||
} else {
|
||||
textEditor.setError(getString(R.string.error_compose_character_limit));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mediaPick = (ImageButton) findViewById(R.id.compose_photo_pick);
|
||||
mediaPick.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onMediaPick();
|
||||
}
|
||||
});
|
||||
markSensitive = (CheckBox) findViewById(R.id.compose_mark_sensitive);
|
||||
markSensitive.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
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,10 +254,13 @@ public class Status {
|
|||
status = new Status(
|
||||
id, accountId, displayName, username, contentPlus, avatar, createdAt,
|
||||
reblogged, favourited, visibility);
|
||||
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