Warn when scheduling a post within 5 minutes (#1698)

* Warn when scheduling a post within 5 minutes

* Fix NPE when scheduled post time isn't set

* Use AlertDialog with option to cancel instead of Toast when a post isn't scheduled far enough in advance

* Move schedule validation warning to scheduling bottom sheet

* Fix scheduling error display when sending after an initially-valid scheduling time has become invalid
This commit is contained in:
Levi Bard 2020-02-25 18:33:25 +01:00 committed by GitHub
parent aba36ca6f8
commit 80e0c55b67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 87 additions and 20 deletions

View file

@ -65,6 +65,7 @@ import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
@ -695,9 +696,16 @@ class ComposeActivity : BaseActivity(),
updateVisibleCharactersLeft() updateVisibleCharactersLeft()
} }
private fun verifyScheduledTime(): Boolean {
return composeScheduleView.verifyScheduledTime(composeScheduleView.getDateTime(viewModel.scheduledAt.value))
}
private fun onSendClicked() { private fun onSendClicked() {
enableButtons(false) if (verifyScheduledTime()) {
sendStatus() sendStatus()
} else {
showScheduleView()
}
} }
/** This is for the fancy keyboards which can insert images and stuff. */ /** This is for the fancy keyboards which can insert images and stuff. */
@ -723,6 +731,7 @@ class ComposeActivity : BaseActivity(),
} }
private fun sendStatus() { private fun sendStatus() {
enableButtons(false)
val contentText = composeEditField.text.toString() val contentText = composeEditField.text.toString()
var spoilerText = "" var spoilerText = ""
if (viewModel.showContentWarning.value!!) { if (viewModel.showContentWarning.value!!) {
@ -974,7 +983,11 @@ class ComposeActivity : BaseActivity(),
override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) { override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) {
composeScheduleView.onTimeSet(hourOfDay, minute) composeScheduleView.onTimeSet(hourOfDay, minute)
viewModel.updateScheduledAt(composeScheduleView.time) viewModel.updateScheduledAt(composeScheduleView.time)
if (verifyScheduledTime()) {
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
} else {
showScheduleView()
}
} }
private fun resetSchedule() { private fun resetSchedule() {

View file

@ -22,6 +22,8 @@ import android.util.AttributeSet;
import android.widget.Button; import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
@ -48,8 +50,10 @@ public class ComposeScheduleView extends ConstraintLayout {
private Button resetScheduleButton; private Button resetScheduleButton;
private TextView scheduledDateTimeView; private TextView scheduledDateTimeView;
private TextView invalidScheduleWarningView;
private Calendar scheduleDateTime; private Calendar scheduleDateTime;
public static int MINIMUM_SCHEDULED_SECONDS = 330; // Minimum is 5 minutes, pad 30 seconds for posting
public ComposeScheduleView(Context context) { public ComposeScheduleView(Context context) {
super(context); super(context);
@ -76,8 +80,10 @@ public class ComposeScheduleView extends ConstraintLayout {
resetScheduleButton = findViewById(R.id.resetScheduleButton); resetScheduleButton = findViewById(R.id.resetScheduleButton);
scheduledDateTimeView = findViewById(R.id.scheduledDateTime); scheduledDateTimeView = findViewById(R.id.scheduledDateTime);
invalidScheduleWarningView = findViewById(R.id.invalidScheduleWarning);
scheduledDateTimeView.setOnClickListener(v -> openPickDateDialog()); scheduledDateTimeView.setOnClickListener(v -> openPickDateDialog());
invalidScheduleWarningView.setText(R.string.warning_scheduling_interval);
scheduleDateTime = null; scheduleDateTime = null;
@ -89,10 +95,13 @@ public class ComposeScheduleView extends ConstraintLayout {
private void setScheduledDateTime() { private void setScheduledDateTime() {
if (scheduleDateTime == null) { if (scheduleDateTime == null) {
scheduledDateTimeView.setText(""); scheduledDateTimeView.setText("");
invalidScheduleWarningView.setVisibility(GONE);
} else { } else {
Date scheduled = scheduleDateTime.getTime();
scheduledDateTimeView.setText(String.format("%s %s", scheduledDateTimeView.setText(String.format("%s %s",
dateFormat.format(scheduleDateTime.getTime()), dateFormat.format(scheduled),
timeFormat.format(scheduleDateTime.getTime()))); timeFormat.format(scheduled)));
verifyScheduledTime(scheduled);
} }
} }
@ -124,9 +133,7 @@ public class ComposeScheduleView extends ConstraintLayout {
.setValidator( .setValidator(
DateValidatorPointForward.from(yesterday)) DateValidatorPointForward.from(yesterday))
.build(); .build();
if (scheduleDateTime == null) { initializeSuggestedTime();
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault());
}
MaterialDatePicker<Long> picker = MaterialDatePicker.Builder MaterialDatePicker<Long> picker = MaterialDatePicker.Builder
.datePicker() .datePicker()
.setSelection(scheduleDateTime.getTimeInMillis()) .setSelection(scheduleDateTime.getTimeInMillis())
@ -147,6 +154,16 @@ public class ComposeScheduleView extends ConstraintLayout {
picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker"); picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker");
} }
public Date getDateTime(String scheduledAt) {
if (scheduledAt != null) {
try {
return iso8601.parse(scheduledAt);
} catch (ParseException e) {
}
}
return null;
}
public void setDateTime(String scheduledAt) { public void setDateTime(String scheduledAt) {
Date date; Date date;
try { try {
@ -154,27 +171,34 @@ public class ComposeScheduleView extends ConstraintLayout {
} catch (ParseException e) { } catch (ParseException e) {
return; return;
} }
if (scheduleDateTime == null) { initializeSuggestedTime();
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault());
}
scheduleDateTime.setTime(date); scheduleDateTime.setTime(date);
setScheduledDateTime(); setScheduledDateTime();
} }
private void onDateSet(long selection) { public boolean verifyScheduledTime(@Nullable Date scheduledTime) {
if (scheduleDateTime == null) { boolean valid;
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault()); if (scheduledTime != null) {
Calendar minimumScheduledTime = getCalendar();
minimumScheduledTime.add(Calendar.SECOND, MINIMUM_SCHEDULED_SECONDS);
valid = scheduledTime.after(minimumScheduledTime.getTime());
} else {
valid = true;
} }
Calendar newDate = Calendar.getInstance(TimeZone.getDefault()); invalidScheduleWarningView.setVisibility(valid ? GONE : VISIBLE);
return valid;
}
private void onDateSet(long selection) {
initializeSuggestedTime();
Calendar newDate = getCalendar();
newDate.setTimeInMillis(selection); newDate.setTimeInMillis(selection);
scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE)); scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE));
openPickTimeDialog(); openPickTimeDialog();
} }
public void onTimeSet(int hourOfDay, int minute) { public void onTimeSet(int hourOfDay, int minute) {
if (scheduleDateTime == null) { initializeSuggestedTime();
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault());
}
scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay); scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
scheduleDateTime.set(Calendar.MINUTE, minute); scheduleDateTime.set(Calendar.MINUTE, minute);
setScheduledDateTime(); setScheduledDateTime();
@ -186,4 +210,16 @@ public class ComposeScheduleView extends ConstraintLayout {
} }
return iso8601.format(scheduleDateTime.getTime()); return iso8601.format(scheduleDateTime.getTime());
} }
@NonNull
public static Calendar getCalendar() {
return Calendar.getInstance(TimeZone.getDefault());
}
private void initializeSuggestedTime() {
if (scheduleDateTime == null) {
scheduleDateTime = getCalendar();
scheduleDateTime.add(Calendar.MINUTE, 15);
}
}
} }

View file

@ -10,7 +10,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:text="@string/action_reset_schedule" android:text="@string/action_reset_schedule"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@id/invalidScheduleWarning"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<TextView <TextView
@ -23,10 +23,27 @@
android:paddingBottom="16dp" android:paddingBottom="16dp"
android:textColor="?android:textColorTertiary" android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@id/invalidScheduleWarning"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1" app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@id/resetScheduleButton" app:layout_constraintStart_toEndOf="@id/resetScheduleButton"
tools:text="2020/01/01 00:00:00" /> tools:text="2020/01/01 00:00:00" />
<TextView
android:id="@+id/invalidScheduleWarning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:paddingStart="4dp"
android:paddingTop="4dp"
android:paddingBottom="16dp"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/warning_scheduling_interval"
android:visibility="gone" />
</merge> </merge>

View file

@ -547,5 +547,6 @@
<string name="no_saved_status">You don\'t have any drafts.</string> <string name="no_saved_status">You don\'t have any drafts.</string>
<string name="no_scheduled_status">You don\'t have any scheduled statuses.</string> <string name="no_scheduled_status">You don\'t have any scheduled statuses.</string>
<string name="warning_scheduling_interval">Mastodon has a minimum scheduling interval of 5 minutes.</string>
</resources> </resources>