Added content warnings to status composer and slightly reworked its design in general.

main
Vavassor 7 years ago
parent 3bd360a7ee
commit 86623c634a
  1. 137
      app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java
  2. 107
      app/src/main/java/com/keylesspalace/tusky/ComposeOptionsFragment.java
  3. 5
      app/src/main/res/drawable/border_background.xml
  4. 7
      app/src/main/res/drawable/ic_options.xml
  5. 61
      app/src/main/res/layout/activity_compose.xml
  6. 52
      app/src/main/res/layout/fragment_compose_options.xml
  7. 2
      app/src/main/res/values/dimens.xml
  8. 10
      app/src/main/res/values/strings.xml

@ -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<QueuedMedia> 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);
}
private void onSendSuccess() {
Toast.makeText(this, getString(R.string.confirmation_send), Toast.LENGTH_SHORT).show();
finish();
}
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;
}
private void onSendFailure(Exception exception) {
textEditor.setError(getString(R.string.error_sending_status));
@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 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<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;
@ -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);
}
}
}

@ -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;
}
}

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="3dp" />
<stroke android:color="#ff000000" android:width="1dp" />
</shape>

@ -0,0 +1,7 @@
<vector android:height="24dp" android:viewportHeight="1133.8583"
android:viewportWidth="1133.8583" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#00000000"
android:pathData="M704.8,566.9A137.9,137.9 0,0 1,566.9 704.8,137.9 137.9,0 0,1 429,566.9 137.9,137.9 0,0 1,566.9 429,137.9 137.9,0 0,1 704.8,566.9ZM566.9,1098.1c-184.7,0 -26,-116.8 -185.9,-209.2 -160,-92.4 -181.8,103.5 -274.1,-56.4 -92.4,-160 88.2,-80.9 88.2,-265.6 0,-184.7 -180.5,-105.6 -88.2,-265.6 92.4,-160 114.1,35.9 274.1,-56.4 160,-92.4 1.2,-209.2 185.9,-209.2 184.7,-0 26,116.8 185.9,209.2 160,92.4 181.8,-103.5 274.1,56.4 92.4,160 -88.2,80.9 -88.2,265.6 0,184.7 180.5,105.6 88.2,265.6C934.6,992.5 912.8,796.6 752.8,888.9 592.9,981.3 751.6,1098.1 566.9,1098.1Z"
android:strokeAlpha="1" android:strokeColor="#000000"
android:strokeLineCap="butt" android:strokeLineJoin="round" android:strokeWidth="68.95068359"/>
</vector>

@ -19,12 +19,31 @@
android:id="@+id/compose_photo_pick"
android:layout_marginLeft="8dp" />
<CheckBox
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_margin="@dimen/compose_mark_sensitive_margin"
android:id="@+id/compose_mark_sensitive"
android:text="@string/action_mark_sensitive" />
<ImageButton
android:layout_width="48dp"
android:layout_height="48dp"
android:id="@+id/compose_options"
app:srcCompat="@drawable/ic_options"
style="?android:attr/borderlessButtonStyle"
android:layout_marginLeft="8dp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/compose_content_warning_bar"
android:background="@drawable/border_background"
android:layout_margin="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:ems="10"
android:id="@+id/field_content_warning" />
</LinearLayout>
@ -59,36 +78,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:id="@+id/radio_visibility"
android:checkedButton="@+id/radio_public">
<RadioButton
android:text="@string/visibility_public"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_public"
android:layout_weight="1" />
<RadioButton
android:text="@string/visibility_unlisted"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_unlisted"
android:layout_weight="1" />
<RadioButton
android:text="@string/visibility_private"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_private"
android:layout_weight="1" />
</RadioGroup>
<Space
android:layout_width="0dp"
android:layout_height="match_parent"

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/radio_visibility"
android:checkedButton="@+id/radio_public"
android:layout_margin="@dimen/compose_options_margin">
<RadioButton
android:text="@string/visibility_public"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_public"
android:layout_weight="1" />
<RadioButton
android:text="@string/visibility_unlisted"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_unlisted"
android:layout_weight="1" />
<RadioButton
android:text="@string/visibility_private"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/radio_private"
android:layout_weight="1" />
</RadioGroup>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_margin="@dimen/compose_options_margin"
android:id="@+id/compose_mark_sensitive"
android:text="@string/action_mark_sensitive" />
<CheckBox
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_margin="@dimen/compose_options_margin"
android:id="@+id/compose_hide_text"
android:text="@string/action_hide_text" />
</LinearLayout>

@ -12,7 +12,7 @@
<dimen name="compose_media_preview_margin">8dp</dimen>
<dimen name="compose_media_preview_margin_bottom">16dp</dimen>
<dimen name="compose_media_preview_side">48dp</dimen>
<dimen name="compose_mark_sensitive_margin">8dp</dimen>
<dimen name="compose_options_margin">8dp</dimen>
<dimen name="notification_icon_vertical_padding">4dp</dimen>
<dimen name="account_note_margin">8dp</dimen>
<dimen name="account_avatar_margin">8dp</dimen>

@ -88,7 +88,9 @@
<string name="action_delete">Delete</string>
<string name="action_send">TOOT</string>
<string name="action_retry">Retry</string>
<string name="action_mark_sensitive">Mark Sensitive</string>
<string name="action_mark_sensitive">Mark media sensitive</string>
<string name="action_hide_text">Hide text behind warning</string>
<string name="action_ok">Ok</string>
<string name="action_cancel">Cancel</string>
<string name="action_back">Back</string>
<string name="action_profile">Profile</string>
@ -99,9 +101,9 @@
<string name="description_domain">Domain</string>
<string name="description_compose">What\'s Happening?</string>
<string name="visibility_public">Public</string>
<string name="visibility_private">Private</string>
<string name="visibility_unlisted">Unlisted</string>
<string name="visibility_public">Show on public timeline</string>
<string name="visibility_unlisted">Do not display on public timeline</string>
<string name="visibility_private">Mark as private</string>
<string name="notification_service_description">Allows Tusky to check for Mastodon notifications.</string>
<string name="notification_service_several_mentions">%d new mentions</string>

Loading…
Cancel
Save