From 86623c634a1ee6ffee7edd715c11a216030abb98 Mon Sep 17 00:00:00 2001 From: Vavassor Date: Fri, 3 Feb 2017 19:53:33 -0500 Subject: [PATCH] Added content warnings to status composer and slightly reworked its design in general. --- .../keylesspalace/tusky/ComposeActivity.java | 141 +++++++++++------- .../tusky/ComposeOptionsFragment.java | 107 +++++++++++++ .../main/res/drawable/border_background.xml | 5 + app/src/main/res/drawable/ic_options.xml | 7 + app/src/main/res/layout/activity_compose.xml | 61 ++++---- .../res/layout/fragment_compose_options.xml | 52 +++++++ app/src/main/res/values/dimens.xml | 2 +- app/src/main/res/values/strings.xml | 10 +- 8 files changed, 294 insertions(+), 91 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/ComposeOptionsFragment.java create mode 100644 app/src/main/res/drawable/border_background.xml create mode 100644 app/src/main/res/drawable/ic_options.xml create mode 100644 app/src/main/res/layout/fragment_compose_options.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index 3b6bfb17..8518b307 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -33,6 +33,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.os.Parcel; import android.provider.OpenableColumns; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -95,10 +96,14 @@ public class ComposeActivity extends AppCompatActivity { private String accessToken; private EditText textEditor; private ImageButton mediaPick; - private CheckBox markSensitive; private LinearLayout mediaPreviewBar; private List mediaQueued; private CountUpDownLatch waitForMediaLatch; + private boolean showMarkSensitive; + private String statusVisibility; // The current values of the options that will be applied + private boolean statusMarkSensitive; // to the status being composed. + private boolean statusHideText; // + private View contentWarningBar; private static class QueuedMedia { public enum Type { @@ -242,8 +247,6 @@ public class ComposeActivity extends AppCompatActivity { 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); @@ -280,31 +283,22 @@ public class ComposeActivity extends AppCompatActivity { mediaQueued = new ArrayList<>(); waitForMediaLatch = new CountUpDownLatch(); - final RadioGroup radio = (RadioGroup) findViewById(R.id.radio_visibility); + contentWarningBar = findViewById(R.id.compose_content_warning_bar); + final EditText contentWarningEditor = (EditText) findViewById(R.id.field_content_warning); + showContentWarning(false); + 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; - } + String spoilerText = ""; + if (statusHideText) { + spoilerText = contentWarningEditor.getText().toString(); } - readyStatus(editable.toString(), visibility, markSensitive.isChecked()); + readyStatus(editable.toString(), statusVisibility, statusMarkSensitive, + spoilerText); } else { textEditor.setError(getString(R.string.error_compose_character_limit)); } @@ -318,20 +312,45 @@ public class ComposeActivity extends AppCompatActivity { onMediaPick(); } }); - markSensitive = (CheckBox) findViewById(R.id.compose_mark_sensitive); - markSensitive.setVisibility(View.GONE); + + ImageButton options = (ImageButton) findViewById(R.id.compose_options); + options.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ComposeOptionsFragment fragment = ComposeOptionsFragment.newInstance( + statusVisibility, statusMarkSensitive, statusHideText, + showMarkSensitive, + new ComposeOptionsFragment.Listener() { + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) {} + + @Override + public void onVisibilityChanged(String visibility) { + statusVisibility = visibility; + } + + @Override + public void onMarkSensitiveChanged(boolean markSensitive) { + statusMarkSensitive = markSensitive; + } + + @Override + public void onContentWarningChanged(boolean hideText) { + showContentWarning(hideText); + } + }); + fragment.show(getSupportFragmentManager(), null); + } + }); } - private void onSendSuccess() { - Toast.makeText(this, getString(R.string.confirmation_send), 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) { + private void sendStatus(String content, String visibility, boolean sensitive, + String spoilerText) { String endpoint = getString(R.string.endpoint_status); String url = "https://" + domain + endpoint; JSONObject parameters = new JSONObject(); @@ -339,6 +358,7 @@ public class ComposeActivity extends AppCompatActivity { parameters.put("status", content); parameters.put("visibility", visibility); parameters.put("sensitive", sensitive); + parameters.put("spoiler_text", spoilerText); if (inReplyToId != null) { parameters.put("in_reply_to_id", inReplyToId); } @@ -350,7 +370,7 @@ public class ComposeActivity extends AppCompatActivity { parameters.put("media_ids", media_ids); } } catch (JSONException e) { - onSendFailure(e); + onSendFailure(); return; } JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, parameters, @@ -362,7 +382,7 @@ public class ComposeActivity extends AppCompatActivity { }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { - onSendFailure(error); + onSendFailure(); } }) { @Override @@ -375,20 +395,26 @@ public class ComposeActivity extends AppCompatActivity { VolleySingleton.getInstance(this).addToRequestQueue(request); } + private void onSendSuccess() { + Toast.makeText(this, getString(R.string.confirmation_send), Toast.LENGTH_SHORT).show(); + finish(); + } + + private void onSendFailure() { + textEditor.setError(getString(R.string.error_sending_status)); + } + private void readyStatus(final String content, final String visibility, - final boolean sensitive) { + final boolean sensitive, final String spoilerText) { final ProgressDialog dialog = ProgressDialog.show(this, "Finishing Media Upload", "Uploading...", true, true); final AsyncTask waitForMediaTask = new AsyncTask() { - private Exception exception; - @Override protected Boolean doInBackground(Void... params) { try { waitForMediaLatch.await(); } catch (InterruptedException e) { - exception = e; return false; } return true; @@ -399,9 +425,9 @@ public class ComposeActivity extends AppCompatActivity { super.onPostExecute(successful); dialog.dismiss(); if (successful) { - sendStatus(content, visibility, sensitive); + sendStatus(content, visibility, sensitive, spoilerText); } else { - onReadyFailure(exception, content, visibility, sensitive); + onReadyFailure(content, visibility, sensitive, spoilerText); } } @@ -423,13 +449,13 @@ public class ComposeActivity extends AppCompatActivity { waitForMediaTask.execute(); } - private void onReadyFailure(Exception exception, final String content, final String visibility, - final boolean sensitive) { + private void onReadyFailure(final String content, final String visibility, + final boolean sensitive, final String spoilerText) { doErrorDialog(R.string.error_media_upload_sending, R.string.action_retry, new View.OnClickListener() { @Override public void onClick(View v) { - readyStatus(content, visibility, sensitive); + readyStatus(content, visibility, sensitive, spoilerText); } }); } @@ -499,7 +525,6 @@ public class ComposeActivity extends AppCompatActivity { } private void addMediaToQueue(QueuedMedia.Type type, Bitmap preview, Uri uri, long mediaSize) { - assert(mediaQueued.size() < Status.MAX_MEDIA_ATTACHMENTS); final QueuedMedia item = new QueuedMedia(type, uri, new ImageView(this)); ImageView view = item.getPreview(); Resources resources = getResources(); @@ -536,7 +561,7 @@ public class ComposeActivity extends AppCompatActivity { disableMediaPicking(); } if (queuedCount >= 1) { - markSensitive.setVisibility(View.VISIBLE); + showMarkSensitive(true); } waitForMediaLatch.countUp(); if (mediaSize > STATUS_MEDIA_SIZE_LIMIT && type == QueuedMedia.Type.IMAGE) { @@ -551,7 +576,7 @@ public class ComposeActivity extends AppCompatActivity { mediaPreviewBar.removeView(item.getPreview()); mediaQueued.remove(item); if (mediaQueued.size() == 0) { - markSensitive.setVisibility(View.GONE); + showMarkSensitive(false); /* If there are no image previews to show, the extra padding that was added to the * EditText can be removed so there isn't unnecessary empty space. */ setPaddingRelative(textEditor, 0, 0, 0, moveBottom); @@ -646,7 +671,7 @@ public class ComposeActivity extends AppCompatActivity { try { item.setId(response.getString("id")); } catch (JSONException e) { - onUploadFailure(item, e); + onUploadFailure(item); return; } waitForMediaLatch.countDown(); @@ -654,7 +679,7 @@ public class ComposeActivity extends AppCompatActivity { }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { - onUploadFailure(item, error); + onUploadFailure(item); } }) { @Override @@ -692,7 +717,7 @@ public class ComposeActivity extends AppCompatActivity { VolleySingleton.getInstance(this).addToRequestQueue(request); } - private void onUploadFailure(QueuedMedia item, @Nullable Exception exception) { + private void onUploadFailure(QueuedMedia item) { displayTransientError(R.string.error_media_upload_sending); removeMediaFromQueue(item); } @@ -770,4 +795,20 @@ public class ComposeActivity extends AppCompatActivity { } } } + + void showMarkSensitive(boolean show) { + showMarkSensitive = show; + if(!showMarkSensitive) { + statusMarkSensitive = false; + } + } + + void showContentWarning(boolean show) { + statusHideText = show; + if (show) { + contentWarningBar.setVisibility(View.VISIBLE); + } else { + contentWarningBar.setVisibility(View.GONE); + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeOptionsFragment.java b/app/src/main/java/com/keylesspalace/tusky/ComposeOptionsFragment.java new file mode 100644 index 00000000..ca78a8c3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeOptionsFragment.java @@ -0,0 +1,107 @@ +package com.keylesspalace.tusky; + +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.support.design.widget.BottomSheetDialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.RadioGroup; + +public class ComposeOptionsFragment extends BottomSheetDialogFragment { + public interface Listener extends Parcelable { + void onVisibilityChanged(String visibility); + void onMarkSensitiveChanged(boolean markSensitive); + void onContentWarningChanged(boolean hideText); + } + + private Listener listener; + + public static ComposeOptionsFragment newInstance(String visibility, boolean markSensitive, + boolean hideText, boolean showMarkSensitive, Listener listener) { + Bundle arguments = new Bundle(); + ComposeOptionsFragment fragment = new ComposeOptionsFragment(); + arguments.putParcelable("listener", listener); + arguments.putString("visibility", visibility); + arguments.putBoolean("markSensitive", markSensitive); + arguments.putBoolean("hideText", hideText); + arguments.putBoolean("showMarkSensitive", showMarkSensitive); + fragment.setArguments(arguments); + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_compose_options, container, false); + + Bundle arguments = getArguments(); + listener = arguments.getParcelable("listener"); + String statusVisibility = arguments.getString("visibility"); + boolean statusMarkSensitive = arguments.getBoolean("markSensitive"); + boolean statusHideText = arguments.getBoolean("hideText"); + boolean showMarkSensitive = arguments.getBoolean("showMarkSensitive"); + + RadioGroup radio = (RadioGroup) rootView.findViewById(R.id.radio_visibility); + int radioCheckedId = R.id.radio_public; + if (statusVisibility != null) { + if (statusVisibility.equals("unlisted")) { + radioCheckedId = R.id.radio_unlisted; + } else if (statusVisibility.equals("private")) { + radioCheckedId = R.id.radio_private; + } + } + radio.check(radioCheckedId); + radio.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + String visibility; + switch (checkedId) { + 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; + } + } + listener.onVisibilityChanged(visibility); + } + }); + + CheckBox markSensitive = (CheckBox) rootView.findViewById(R.id.compose_mark_sensitive); + if (showMarkSensitive) { + markSensitive.setChecked(statusMarkSensitive); + markSensitive.setEnabled(true); + markSensitive.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + listener.onMarkSensitiveChanged(isChecked); + } + }); + } else { + markSensitive.setEnabled(false); + } + + CheckBox hideText = (CheckBox) rootView.findViewById(R.id.compose_hide_text); + hideText.setChecked(statusHideText); + hideText.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + listener.onContentWarningChanged(isChecked); + } + }); + + return rootView; + } +} diff --git a/app/src/main/res/drawable/border_background.xml b/app/src/main/res/drawable/border_background.xml new file mode 100644 index 00000000..6fe0dccb --- /dev/null +++ b/app/src/main/res/drawable/border_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_options.xml b/app/src/main/res/drawable/ic_options.xml new file mode 100644 index 00000000..7981620b --- /dev/null +++ b/app/src/main/res/drawable/ic_options.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index 8a3693af..a2586b86 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -19,12 +19,31 @@ android:id="@+id/compose_photo_pick" android:layout_marginLeft="8dp" /> - + + + + + + + @@ -59,36 +78,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a0c300fd..cb38da87 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -12,7 +12,7 @@ 8dp 16dp 48dp - 8dp + 8dp 4dp 8dp 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8089c6eb..9f6edb2c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,7 +88,9 @@ Delete TOOT Retry - Mark Sensitive + Mark media sensitive + Hide text behind warning + Ok Cancel Back Profile @@ -99,9 +101,9 @@ Domain What\'s Happening? - Public - Private - Unlisted + Show on public timeline + Do not display on public timeline + Mark as private Allows Tusky to check for Mastodon notifications. %d new mentions