Scheduled toot (#1004)
* Scheduled toot * Hide scheduled toot button if version < 2.7.0 * Fix timeline reloading after toot * Add edit icon to ComposeScheduleView * Add button to reset scheduled toot * Close bottom sheet and change button color after time a was selected * Fix edit icon's size * List of scheduled toots * Fix instance version check * Use MaterialDatePicker * Set date and time consecutively * Add licenses
This commit is contained in:
parent
a6b9d2f67e
commit
9e4c19a47e
23 changed files with 933 additions and 56 deletions
|
@ -100,7 +100,7 @@ dependencies {
|
||||||
implementation 'androidx.browser:browser:1.0.0'
|
implementation 'androidx.browser:browser:1.0.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||||
implementation 'com.google.android.material:material:1.1.0-alpha05'
|
implementation 'com.google.android.material:material:1.1.0-alpha10'
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.preference:preference:1.1.0-alpha04'
|
implementation 'androidx.preference:preference:1.1.0-alpha04'
|
||||||
|
|
|
@ -135,6 +135,7 @@
|
||||||
android:name=".components.report.ReportActivity"
|
android:name=".components.report.ReportActivity"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||||
<activity android:name=".components.instancemute.InstanceListActivity" />
|
<activity android:name=".components.instancemute.InstanceListActivity" />
|
||||||
|
<activity android:name=".ScheduledTootActivity" />
|
||||||
|
|
||||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||||
<receiver
|
<receiver
|
||||||
|
|
|
@ -17,7 +17,9 @@ package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.DatePickerDialog;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
|
import android.app.TimePickerDialog;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
@ -55,14 +57,35 @@ import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.DatePicker;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.PopupMenu;
|
import android.widget.PopupMenu;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.TimePicker;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.Px;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.content.FileProvider;
|
||||||
|
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||||
|
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||||
|
import androidx.lifecycle.Lifecycle;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.transition.TransitionManager;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
@ -95,9 +118,11 @@ import com.keylesspalace.tusky.util.SaveTootHelper;
|
||||||
import com.keylesspalace.tusky.util.SpanUtilsKt;
|
import com.keylesspalace.tusky.util.SpanUtilsKt;
|
||||||
import com.keylesspalace.tusky.util.StringUtils;
|
import com.keylesspalace.tusky.util.StringUtils;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
import com.keylesspalace.tusky.util.VersionUtils;
|
||||||
import com.keylesspalace.tusky.view.AddPollDialog;
|
import com.keylesspalace.tusky.view.AddPollDialog;
|
||||||
import com.keylesspalace.tusky.view.ComposeOptionsListener;
|
import com.keylesspalace.tusky.view.ComposeOptionsListener;
|
||||||
import com.keylesspalace.tusky.view.ComposeOptionsView;
|
import com.keylesspalace.tusky.view.ComposeOptionsView;
|
||||||
|
import com.keylesspalace.tusky.view.ComposeScheduleView;
|
||||||
import com.keylesspalace.tusky.view.EditTextTyped;
|
import com.keylesspalace.tusky.view.EditTextTyped;
|
||||||
import com.keylesspalace.tusky.view.PollPreviewView;
|
import com.keylesspalace.tusky.view.PollPreviewView;
|
||||||
import com.keylesspalace.tusky.view.ProgressImageView;
|
import com.keylesspalace.tusky.view.ProgressImageView;
|
||||||
|
@ -123,25 +148,6 @@ import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.Px;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.core.content.FileProvider;
|
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
|
||||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
|
||||||
import androidx.lifecycle.Lifecycle;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.transition.TransitionManager;
|
|
||||||
|
|
||||||
import at.connyduck.sparkbutton.helpers.Utils;
|
import at.connyduck.sparkbutton.helpers.Utils;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
import io.reactivex.SingleObserver;
|
import io.reactivex.SingleObserver;
|
||||||
|
@ -169,7 +175,8 @@ public final class ComposeActivity
|
||||||
implements ComposeOptionsListener,
|
implements ComposeOptionsListener,
|
||||||
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||||
OnEmojiSelectedListener,
|
OnEmojiSelectedListener,
|
||||||
Injectable, InputConnectionCompat.OnCommitContentListener {
|
Injectable, InputConnectionCompat.OnCommitContentListener,
|
||||||
|
TimePickerDialog.OnTimeSetListener {
|
||||||
|
|
||||||
private static final String TAG = "ComposeActivity"; // logging tag
|
private static final String TAG = "ComposeActivity"; // logging tag
|
||||||
static final int STATUS_CHARACTER_LIMIT = 500;
|
static final int STATUS_CHARACTER_LIMIT = 500;
|
||||||
|
@ -192,6 +199,7 @@ public final class ComposeActivity
|
||||||
private static final String REPLYING_STATUS_AUTHOR_USERNAME_EXTRA = "replying_author_nickname_extra";
|
private static final String REPLYING_STATUS_AUTHOR_USERNAME_EXTRA = "replying_author_nickname_extra";
|
||||||
private static final String REPLYING_STATUS_CONTENT_EXTRA = "replying_status_content";
|
private static final String REPLYING_STATUS_CONTENT_EXTRA = "replying_status_content";
|
||||||
private static final String MEDIA_ATTACHMENTS_EXTRA = "media_attachments";
|
private static final String MEDIA_ATTACHMENTS_EXTRA = "media_attachments";
|
||||||
|
private static final String SCHEDULED_AT_EXTRA = "scheduled_at";
|
||||||
private static final String SENSITIVE_EXTRA = "sensitive";
|
private static final String SENSITIVE_EXTRA = "sensitive";
|
||||||
private static final String POLL_EXTRA = "poll";
|
private static final String POLL_EXTRA = "poll";
|
||||||
// Mastodon only counts URLs as this long in terms of status character limits
|
// Mastodon only counts URLs as this long in terms of status character limits
|
||||||
|
@ -217,6 +225,7 @@ public final class ComposeActivity
|
||||||
private ImageButton contentWarningButton;
|
private ImageButton contentWarningButton;
|
||||||
private ImageButton emojiButton;
|
private ImageButton emojiButton;
|
||||||
private ImageButton hideMediaToggle;
|
private ImageButton hideMediaToggle;
|
||||||
|
private ImageButton scheduleButton;
|
||||||
private TextView actionAddPoll;
|
private TextView actionAddPoll;
|
||||||
private Button atButton;
|
private Button atButton;
|
||||||
private Button hashButton;
|
private Button hashButton;
|
||||||
|
@ -225,6 +234,8 @@ public final class ComposeActivity
|
||||||
private BottomSheetBehavior composeOptionsBehavior;
|
private BottomSheetBehavior composeOptionsBehavior;
|
||||||
private BottomSheetBehavior addMediaBehavior;
|
private BottomSheetBehavior addMediaBehavior;
|
||||||
private BottomSheetBehavior emojiBehavior;
|
private BottomSheetBehavior emojiBehavior;
|
||||||
|
private BottomSheetBehavior scheduleBehavior;
|
||||||
|
private ComposeScheduleView scheduleView;
|
||||||
private RecyclerView emojiView;
|
private RecyclerView emojiView;
|
||||||
|
|
||||||
private PollPreviewView pollPreview;
|
private PollPreviewView pollPreview;
|
||||||
|
@ -278,6 +289,8 @@ public final class ComposeActivity
|
||||||
contentWarningButton = findViewById(R.id.composeContentWarningButton);
|
contentWarningButton = findViewById(R.id.composeContentWarningButton);
|
||||||
emojiButton = findViewById(R.id.composeEmojiButton);
|
emojiButton = findViewById(R.id.composeEmojiButton);
|
||||||
hideMediaToggle = findViewById(R.id.composeHideMediaButton);
|
hideMediaToggle = findViewById(R.id.composeHideMediaButton);
|
||||||
|
scheduleButton = findViewById(R.id.composeScheduleButton);
|
||||||
|
scheduleView = findViewById(R.id.composeScheduleView);
|
||||||
emojiView = findViewById(R.id.emojiView);
|
emojiView = findViewById(R.id.emojiView);
|
||||||
emojiList = Collections.emptyList();
|
emojiList = Collections.emptyList();
|
||||||
atButton = findViewById(R.id.atButton);
|
atButton = findViewById(R.id.atButton);
|
||||||
|
@ -361,6 +374,8 @@ public final class ComposeActivity
|
||||||
|
|
||||||
addMediaBehavior = BottomSheetBehavior.from(findViewById(R.id.addMediaBottomSheet));
|
addMediaBehavior = BottomSheetBehavior.from(findViewById(R.id.addMediaBottomSheet));
|
||||||
|
|
||||||
|
scheduleBehavior = BottomSheetBehavior.from(scheduleView);
|
||||||
|
|
||||||
emojiBehavior = BottomSheetBehavior.from(emojiView);
|
emojiBehavior = BottomSheetBehavior.from(emojiView);
|
||||||
|
|
||||||
emojiView.setLayoutManager(new GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false));
|
emojiView.setLayoutManager(new GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false));
|
||||||
|
@ -374,6 +389,8 @@ public final class ComposeActivity
|
||||||
contentWarningButton.setOnClickListener(v -> onContentWarningChanged());
|
contentWarningButton.setOnClickListener(v -> onContentWarningChanged());
|
||||||
emojiButton.setOnClickListener(v -> showEmojis());
|
emojiButton.setOnClickListener(v -> showEmojis());
|
||||||
hideMediaToggle.setOnClickListener(v -> toggleHideMedia());
|
hideMediaToggle.setOnClickListener(v -> toggleHideMedia());
|
||||||
|
scheduleButton.setOnClickListener(v -> showScheduleView());
|
||||||
|
scheduleView.setResetOnClickListener(v -> resetSchedule());
|
||||||
atButton.setOnClickListener(v -> atButtonClicked());
|
atButton.setOnClickListener(v -> atButtonClicked());
|
||||||
hashButton.setOnClickListener(v -> hashButtonClicked());
|
hashButton.setOnClickListener(v -> hashButtonClicked());
|
||||||
|
|
||||||
|
@ -521,6 +538,11 @@ public final class ComposeActivity
|
||||||
replyContentTextView.setText(intent.getStringExtra(REPLYING_STATUS_CONTENT_EXTRA));
|
replyContentTextView.setText(intent.getStringExtra(REPLYING_STATUS_CONTENT_EXTRA));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String scheduledAt = intent.getStringExtra(SCHEDULED_AT_EXTRA);
|
||||||
|
if (!TextUtils.isEmpty(scheduledAt)) {
|
||||||
|
scheduleView.setDateTime(scheduledAt);
|
||||||
|
}
|
||||||
|
|
||||||
statusMarkSensitive = intent.getBooleanExtra(SENSITIVE_EXTRA, statusMarkSensitive);
|
statusMarkSensitive = intent.getBooleanExtra(SENSITIVE_EXTRA, statusMarkSensitive);
|
||||||
|
|
||||||
if(intent.hasExtra(POLL_EXTRA) && (mediaAttachments == null || mediaAttachments.size() == 0)) {
|
if(intent.hasExtra(POLL_EXTRA) && (mediaAttachments == null || mediaAttachments.size() == 0)) {
|
||||||
|
@ -536,6 +558,7 @@ public final class ComposeActivity
|
||||||
setStatusVisibility(startingVisibility);
|
setStatusVisibility(startingVisibility);
|
||||||
|
|
||||||
updateHideMediaToggle();
|
updateHideMediaToggle();
|
||||||
|
updateScheduleButton();
|
||||||
updateVisibleCharactersLeft();
|
updateVisibleCharactersLeft();
|
||||||
|
|
||||||
// Setup the main text field.
|
// Setup the main text field.
|
||||||
|
@ -799,11 +822,22 @@ public final class ComposeActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateScheduleButton() {
|
||||||
|
@ColorInt int color;
|
||||||
|
if(scheduleView.getTime() == null) {
|
||||||
|
color = ThemeUtils.getColor(this, android.R.attr.textColorTertiary);
|
||||||
|
} else {
|
||||||
|
color = ContextCompat.getColor(this, R.color.tusky_blue);
|
||||||
|
}
|
||||||
|
scheduleButton.getDrawable().setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||||
|
}
|
||||||
|
|
||||||
private void disableButtons() {
|
private void disableButtons() {
|
||||||
pickButton.setClickable(false);
|
pickButton.setClickable(false);
|
||||||
visibilityButton.setClickable(false);
|
visibilityButton.setClickable(false);
|
||||||
emojiButton.setClickable(false);
|
emojiButton.setClickable(false);
|
||||||
hideMediaToggle.setClickable(false);
|
hideMediaToggle.setClickable(false);
|
||||||
|
scheduleButton.setClickable(false);
|
||||||
tootButton.setEnabled(false);
|
tootButton.setEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -812,6 +846,7 @@ public final class ComposeActivity
|
||||||
visibilityButton.setClickable(true);
|
visibilityButton.setClickable(true);
|
||||||
emojiButton.setClickable(true);
|
emojiButton.setClickable(true);
|
||||||
hideMediaToggle.setClickable(true);
|
hideMediaToggle.setClickable(true);
|
||||||
|
scheduleButton.setClickable(true);
|
||||||
tootButton.setEnabled(true);
|
tootButton.setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -859,12 +894,23 @@ public final class ComposeActivity
|
||||||
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
} else {
|
} else {
|
||||||
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showScheduleView() {
|
||||||
|
if (scheduleBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN || scheduleBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
|
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
} else {
|
||||||
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void showEmojis() {
|
private void showEmojis() {
|
||||||
|
|
||||||
if (emojiView.getAdapter() != null) {
|
if (emojiView.getAdapter() != null) {
|
||||||
|
@ -876,7 +922,7 @@ public final class ComposeActivity
|
||||||
emojiBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
emojiBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
} else {
|
} else {
|
||||||
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
}
|
}
|
||||||
|
@ -891,7 +937,7 @@ public final class ComposeActivity
|
||||||
addMediaBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
addMediaBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
} else {
|
} else {
|
||||||
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
}
|
}
|
||||||
|
@ -1084,7 +1130,8 @@ public final class ComposeActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent sendIntent = SendTootService.sendTootIntent(this, content, spoilerText,
|
Intent sendIntent = SendTootService.sendTootIntent(this, content, spoilerText,
|
||||||
visibility, !mediaUris.isEmpty() && sensitive, mediaIds, mediaUris, mediaDescriptions, inReplyToId, poll,
|
visibility, !mediaUris.isEmpty() && sensitive, mediaIds, mediaUris, mediaDescriptions,
|
||||||
|
scheduleView.getTime(), inReplyToId, poll,
|
||||||
getIntent().getStringExtra(REPLYING_STATUS_CONTENT_EXTRA),
|
getIntent().getStringExtra(REPLYING_STATUS_CONTENT_EXTRA),
|
||||||
getIntent().getStringExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA),
|
getIntent().getStringExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA),
|
||||||
getIntent().getStringExtra(SAVED_JSON_URLS_EXTRA),
|
getIntent().getStringExtra(SAVED_JSON_URLS_EXTRA),
|
||||||
|
@ -1744,10 +1791,12 @@ public final class ComposeActivity
|
||||||
// Acting like a teen: deliberately ignoring parent.
|
// Acting like a teen: deliberately ignoring parent.
|
||||||
if (composeOptionsBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED ||
|
if (composeOptionsBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
addMediaBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED ||
|
addMediaBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
emojiBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
emojiBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
scheduleBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1947,6 +1996,10 @@ public final class ComposeActivity
|
||||||
updateVisibleCharactersLeft();
|
updateVisibleCharactersLeft();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!new VersionUtils(instance.getVersion()).supportsScheduledToots()) {
|
||||||
|
scheduleButton.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
if (instance.getPollLimits() != null) {
|
if (instance.getPollLimits() != null) {
|
||||||
maxPollOptions = instance.getPollLimits().getMaxOptions();
|
maxPollOptions = instance.getPollLimits().getMaxOptions();
|
||||||
maxPollOptionLength = instance.getPollLimits().getMaxOptionChars();
|
maxPollOptionLength = instance.getPollLimits().getMaxOptionChars();
|
||||||
|
@ -2048,6 +2101,19 @@ public final class ComposeActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
|
||||||
|
scheduleView.onTimeSet(hourOfDay, minute);
|
||||||
|
updateScheduleButton();
|
||||||
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetSchedule() {
|
||||||
|
scheduleView.resetSchedule();
|
||||||
|
updateScheduleButton();
|
||||||
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
public static final class IntentBuilder {
|
public static final class IntentBuilder {
|
||||||
@Nullable
|
@Nullable
|
||||||
private Integer savedTootUid;
|
private Integer savedTootUid;
|
||||||
|
@ -2074,6 +2140,8 @@ public final class ComposeActivity
|
||||||
@Nullable
|
@Nullable
|
||||||
private ArrayList<Attachment> mediaAttachments;
|
private ArrayList<Attachment> mediaAttachments;
|
||||||
@Nullable
|
@Nullable
|
||||||
|
private String scheduledAt;
|
||||||
|
@Nullable
|
||||||
private Boolean sensitive;
|
private Boolean sensitive;
|
||||||
@Nullable
|
@Nullable
|
||||||
private NewPoll poll;
|
private NewPoll poll;
|
||||||
|
@ -2138,6 +2206,11 @@ public final class ComposeActivity
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IntentBuilder scheduledAt(String scheduledAt) {
|
||||||
|
this.scheduledAt = scheduledAt;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public IntentBuilder sensitive(boolean sensitive) {
|
public IntentBuilder sensitive(boolean sensitive) {
|
||||||
this.sensitive = sensitive;
|
this.sensitive = sensitive;
|
||||||
return this;
|
return this;
|
||||||
|
@ -2188,6 +2261,9 @@ public final class ComposeActivity
|
||||||
if (mediaAttachments != null) {
|
if (mediaAttachments != null) {
|
||||||
intent.putParcelableArrayListExtra(MEDIA_ATTACHMENTS_EXTRA, mediaAttachments);
|
intent.putParcelableArrayListExtra(MEDIA_ATTACHMENTS_EXTRA, mediaAttachments);
|
||||||
}
|
}
|
||||||
|
if (scheduledAt != null) {
|
||||||
|
intent.putExtra(SCHEDULED_AT_EXTRA, scheduledAt);
|
||||||
|
}
|
||||||
if (sensitive != null) {
|
if (sensitive != null) {
|
||||||
intent.putExtra(SENSITIVE_EXTRA, sensitive);
|
intent.putExtra(SENSITIVE_EXTRA, sensitive);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,26 +15,11 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import androidx.lifecycle.Lifecycle;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
|
|
||||||
import androidx.emoji.text.EmojiCompat;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
@ -42,6 +27,17 @@ import android.view.KeyEvent;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.emoji.text.EmojiCompat;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.Lifecycle;
|
||||||
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
|
import com.google.android.material.tabs.TabLayout;
|
||||||
import com.keylesspalace.tusky.appstore.CacheUpdater;
|
import com.keylesspalace.tusky.appstore.CacheUpdater;
|
||||||
import com.keylesspalace.tusky.appstore.EventHub;
|
import com.keylesspalace.tusky.appstore.EventHub;
|
||||||
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent;
|
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent;
|
||||||
|
@ -101,6 +97,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
private static final long DRAWER_ITEM_ABOUT = 7;
|
private static final long DRAWER_ITEM_ABOUT = 7;
|
||||||
private static final long DRAWER_ITEM_LOG_OUT = 8;
|
private static final long DRAWER_ITEM_LOG_OUT = 8;
|
||||||
private static final long DRAWER_ITEM_FOLLOW_REQUESTS = 9;
|
private static final long DRAWER_ITEM_FOLLOW_REQUESTS = 9;
|
||||||
|
private static final long DRAWER_ITEM_SCHEDULED_TOOT = 10;
|
||||||
public static final String STATUS_URL = "statusUrl";
|
public static final String STATUS_URL = "statusUrl";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -391,6 +388,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_LISTS).withName(R.string.action_lists).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_list));
|
listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_LISTS).withName(R.string.action_lists).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_list));
|
||||||
listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SEARCH).withName(R.string.action_search).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_search));
|
listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SEARCH).withName(R.string.action_search).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_search));
|
||||||
listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SAVED_TOOT).withName(R.string.action_access_saved_toot).withSelectable(false).withIcon(R.drawable.ic_notebook).withIconTintingEnabled(true));
|
listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SAVED_TOOT).withName(R.string.action_access_saved_toot).withSelectable(false).withIcon(R.drawable.ic_notebook).withIconTintingEnabled(true));
|
||||||
|
listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SCHEDULED_TOOT).withName(R.string.action_access_scheduled_toot).withSelectable(false).withIcon(R.drawable.ic_access_time).withIconTintingEnabled(true));
|
||||||
listItems.add(new DividerDrawerItem());
|
listItems.add(new DividerDrawerItem());
|
||||||
listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_ACCOUNT_SETTINGS).withName(R.string.action_view_account_preferences).withSelectable(false).withIcon(R.drawable.ic_account_settings).withIconTintingEnabled(true));
|
listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_ACCOUNT_SETTINGS).withName(R.string.action_view_account_preferences).withSelectable(false).withIcon(R.drawable.ic_account_settings).withIconTintingEnabled(true));
|
||||||
listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_SETTINGS).withName(R.string.action_view_preferences).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings));
|
listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_SETTINGS).withName(R.string.action_view_preferences).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings));
|
||||||
|
@ -433,6 +431,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
} else if (drawerItemIdentifier == DRAWER_ITEM_SAVED_TOOT) {
|
} else if (drawerItemIdentifier == DRAWER_ITEM_SAVED_TOOT) {
|
||||||
Intent intent = new Intent(MainActivity.this, SavedTootActivity.class);
|
Intent intent = new Intent(MainActivity.this, SavedTootActivity.class);
|
||||||
startActivityWithSlideInAnimation(intent);
|
startActivityWithSlideInAnimation(intent);
|
||||||
|
} else if (drawerItemIdentifier == DRAWER_ITEM_SCHEDULED_TOOT) {
|
||||||
|
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(this));
|
||||||
} else if (drawerItemIdentifier == DRAWER_ITEM_LISTS) {
|
} else if (drawerItemIdentifier == DRAWER_ITEM_LISTS) {
|
||||||
startActivityWithSlideInAnimation(ListsActivity.newIntent(this));
|
startActivityWithSlideInAnimation(ListsActivity.newIntent(this));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.keylesspalace.tusky.adapter.ScheduledTootAdapter
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||||
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.uber.autodispose.AutoDispose.autoDisposable
|
||||||
|
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import kotlinx.android.synthetic.main.activity_scheduled_toot.*
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduledTootActivity : BaseActivity(), ScheduledTootAdapter.ScheduledTootAction, Injectable {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun newIntent(context: Context): Intent {
|
||||||
|
return Intent(context, ScheduledTootActivity::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lateinit var adapter: ScheduledTootAdapter
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var mastodonApi: MastodonApi
|
||||||
|
@Inject
|
||||||
|
lateinit var eventHub: EventHub
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_scheduled_toot)
|
||||||
|
|
||||||
|
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||||
|
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
val bar = supportActionBar
|
||||||
|
if (bar != null) {
|
||||||
|
bar.title = getString(R.string.title_scheduled_toot)
|
||||||
|
bar.setDisplayHomeAsUpEnabled(true)
|
||||||
|
bar.setDisplayShowHomeEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
swipe_refresh_layout.setOnRefreshListener(this::refreshStatuses)
|
||||||
|
|
||||||
|
scheduled_toot_list.setHasFixedSize(true)
|
||||||
|
val layoutManager = LinearLayoutManager(this)
|
||||||
|
scheduled_toot_list.layoutManager = layoutManager
|
||||||
|
val divider = DividerItemDecoration(this, layoutManager.orientation)
|
||||||
|
scheduled_toot_list.addItemDecoration(divider)
|
||||||
|
adapter = ScheduledTootAdapter(this)
|
||||||
|
scheduled_toot_list.adapter = adapter
|
||||||
|
|
||||||
|
loadStatuses()
|
||||||
|
|
||||||
|
eventHub.events
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.`as`(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||||
|
.subscribe { event ->
|
||||||
|
if (event is StatusScheduledEvent) {
|
||||||
|
refreshStatuses()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadStatuses() {
|
||||||
|
progress_bar.visibility = View.VISIBLE
|
||||||
|
mastodonApi.scheduledStatuses()
|
||||||
|
.enqueue(object : Callback<List<ScheduledStatus>> {
|
||||||
|
override fun onResponse(call: Call<List<ScheduledStatus>>, response: Response<List<ScheduledStatus>>) {
|
||||||
|
progress_bar.visibility = View.GONE
|
||||||
|
if (response.body().isNullOrEmpty()) {
|
||||||
|
errorMessageView.show()
|
||||||
|
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
|
||||||
|
null)
|
||||||
|
} else {
|
||||||
|
show(response.body()!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<List<ScheduledStatus>>, t: Throwable) {
|
||||||
|
progress_bar.visibility = View.GONE
|
||||||
|
errorMessageView.show()
|
||||||
|
errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
||||||
|
errorMessageView.hide()
|
||||||
|
loadStatuses()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshStatuses() {
|
||||||
|
swipe_refresh_layout.isRefreshing = true
|
||||||
|
mastodonApi.scheduledStatuses()
|
||||||
|
.enqueue(object : Callback<List<ScheduledStatus>> {
|
||||||
|
override fun onResponse(call: Call<List<ScheduledStatus>>, response: Response<List<ScheduledStatus>>) {
|
||||||
|
swipe_refresh_layout.isRefreshing = false
|
||||||
|
if (response.body().isNullOrEmpty()) {
|
||||||
|
errorMessageView.show()
|
||||||
|
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
|
||||||
|
null)
|
||||||
|
} else {
|
||||||
|
show(response.body()!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<List<ScheduledStatus>>, t: Throwable) {
|
||||||
|
swipe_refresh_layout.isRefreshing = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(statuses: List<ScheduledStatus>) {
|
||||||
|
adapter.setItems(statuses)
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun edit(position: Int, item: ScheduledStatus?) {
|
||||||
|
if (item == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val intent = ComposeActivity.IntentBuilder()
|
||||||
|
.tootText(item.params.text)
|
||||||
|
.contentWarning(item.params.spoilerText)
|
||||||
|
.mediaAttachments(item.mediaAttachments)
|
||||||
|
.inReplyToId(item.params.inReplyToId)
|
||||||
|
.visibility(item.params.visibility)
|
||||||
|
.scheduledAt(item.scheduledAt)
|
||||||
|
.sensitive(item.params.sensitive)
|
||||||
|
.build(this)
|
||||||
|
startActivity(intent)
|
||||||
|
delete(position, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(position: Int, item: ScheduledStatus?) {
|
||||||
|
if (item == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mastodonApi.deleteScheduledStatus(item.id)
|
||||||
|
.enqueue(object : Callback<ResponseBody> {
|
||||||
|
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
|
||||||
|
adapter.removeItem(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
/* Copyright 2019 kyori19
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.adapter;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.entity.ScheduledStatus;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ScheduledTootAdapter extends RecyclerView.Adapter {
|
||||||
|
private List<ScheduledStatus> list;
|
||||||
|
private ScheduledTootAction handler;
|
||||||
|
|
||||||
|
public ScheduledTootAdapter(Context context) {
|
||||||
|
super();
|
||||||
|
list = new ArrayList<>();
|
||||||
|
handler = (ScheduledTootAction) context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_scheduled_toot, parent, false);
|
||||||
|
return new TootViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
|
TootViewHolder holder = (TootViewHolder) viewHolder;
|
||||||
|
holder.bind(getItem(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return list.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItems(List<ScheduledStatus> newToot) {
|
||||||
|
list = new ArrayList<>();
|
||||||
|
list.addAll(newToot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public ScheduledStatus removeItem(int position) {
|
||||||
|
if (position < 0 || position >= list.size()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ScheduledStatus toot = list.remove(position);
|
||||||
|
notifyItemRemoved(position);
|
||||||
|
return toot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScheduledStatus getItem(int position) {
|
||||||
|
if (position >= 0 && position < list.size()) {
|
||||||
|
return list.get(position);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ScheduledTootAction {
|
||||||
|
void edit(int position, ScheduledStatus item);
|
||||||
|
|
||||||
|
void delete(int position, ScheduledStatus item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TootViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
View view;
|
||||||
|
TextView text;
|
||||||
|
ImageButton edit;
|
||||||
|
ImageButton delete;
|
||||||
|
|
||||||
|
TootViewHolder(View view) {
|
||||||
|
super(view);
|
||||||
|
this.view = view;
|
||||||
|
this.text = view.findViewById(R.id.text);
|
||||||
|
this.edit = view.findViewById(R.id.edit);
|
||||||
|
this.delete = view.findViewById(R.id.delete);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bind(final ScheduledStatus item) {
|
||||||
|
edit.setEnabled(true);
|
||||||
|
delete.setEnabled(true);
|
||||||
|
|
||||||
|
if (item != null) {
|
||||||
|
text.setText(item.getParams().getText());
|
||||||
|
|
||||||
|
edit.setOnClickListener(v -> {
|
||||||
|
v.setEnabled(false);
|
||||||
|
handler.edit(getAdapterPosition(), item);
|
||||||
|
});
|
||||||
|
|
||||||
|
delete.setOnClickListener(v -> {
|
||||||
|
v.setEnabled(false);
|
||||||
|
handler.delete(getAdapterPosition(), item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ data class BlockEvent(val accountId: String) : Dispatchable
|
||||||
data class MuteEvent(val accountId: String) : Dispatchable
|
data class MuteEvent(val accountId: String) : Dispatchable
|
||||||
data class StatusDeletedEvent(val statusId: String) : Dispatchable
|
data class StatusDeletedEvent(val statusId: String) : Dispatchable
|
||||||
data class StatusComposedEvent(val status: Status) : Dispatchable
|
data class StatusComposedEvent(val status: Status) : Dispatchable
|
||||||
|
data class StatusScheduledEvent(val status: Status) : Dispatchable
|
||||||
data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
|
data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
|
||||||
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
||||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
||||||
|
|
|
@ -97,4 +97,7 @@ abstract class ActivitiesModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributesScheduledTootActivity(): ScheduledTootActivity
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ data class NewStatus(
|
||||||
val visibility: String,
|
val visibility: String,
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
@SerializedName("media_ids") val mediaIds: List<String>?,
|
@SerializedName("media_ids") val mediaIds: List<String>?,
|
||||||
|
@SerializedName("scheduled_at") val scheduledAt: String?,
|
||||||
val poll: NewPoll?
|
val poll: NewPoll?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* Copyright 2019 kyori19
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class ScheduledStatus(
|
||||||
|
val id: String,
|
||||||
|
@SerializedName("scheduled_at") val scheduledAt: String,
|
||||||
|
val params: StatusParams,
|
||||||
|
@SerializedName("media_attachments") val mediaAttachments: ArrayList<Attachment>
|
||||||
|
)
|
|
@ -0,0 +1,26 @@
|
||||||
|
/* Copyright 2019 kyori19
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class StatusParams(
|
||||||
|
val text: String,
|
||||||
|
val sensitive: Boolean,
|
||||||
|
val visibility: Status.Visibility,
|
||||||
|
@SerializedName("spoiler_text") val spoilerText: String,
|
||||||
|
@SerializedName("in_reply_to_id") val inReplyToId: String?
|
||||||
|
)
|
|
@ -0,0 +1,53 @@
|
||||||
|
/* Copyright 2019 kyori19
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.fragment;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.app.TimePickerDialog;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.ComposeActivity;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
public class TimePickerFragment extends DialogFragment {
|
||||||
|
|
||||||
|
public static final String PICKER_TIME_HOUR = "picker_time_hour";
|
||||||
|
public static final String PICKER_TIME_MINUTE = "picker_time_minute";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
Bundle args = getArguments();
|
||||||
|
Calendar calendar = Calendar.getInstance(TimeZone.getDefault());
|
||||||
|
if (args != null) {
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, args.getInt(PICKER_TIME_HOUR));
|
||||||
|
calendar.set(Calendar.MINUTE, args.getInt(PICKER_TIME_MINUTE));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TimePickerDialog(getContext(),
|
||||||
|
android.R.style.Theme_DeviceDefault_Dialog,
|
||||||
|
(ComposeActivity) getActivity(),
|
||||||
|
calendar.get(Calendar.HOUR_OF_DAY),
|
||||||
|
calendar.get(Calendar.MINUTE),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -23,20 +23,8 @@ import okhttp3.RequestBody
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.*
|
||||||
import retrofit2.http.DELETE
|
|
||||||
import retrofit2.http.Field
|
import retrofit2.http.Field
|
||||||
import retrofit2.http.FormUrlEncoded
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.HTTP
|
|
||||||
import retrofit2.http.Header
|
|
||||||
import retrofit2.http.Multipart
|
|
||||||
import retrofit2.http.PATCH
|
|
||||||
import retrofit2.http.POST
|
|
||||||
import retrofit2.http.PUT
|
|
||||||
import retrofit2.http.Part
|
|
||||||
import retrofit2.http.Path
|
|
||||||
import retrofit2.http.Query
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* for documentation of the Mastodon REST API see https://docs.joinmastodon.org/api/
|
* for documentation of the Mastodon REST API see https://docs.joinmastodon.org/api/
|
||||||
|
@ -202,6 +190,14 @@ interface MastodonApi {
|
||||||
@Path("id") statusId: String
|
@Path("id") statusId: String
|
||||||
): Single<Status>
|
): Single<Status>
|
||||||
|
|
||||||
|
@GET("api/v1/scheduled_statuses")
|
||||||
|
fun scheduledStatuses(): Call<List<ScheduledStatus>>
|
||||||
|
|
||||||
|
@DELETE("api/v1/scheduled_statuses/{id}")
|
||||||
|
fun deleteScheduledStatus(
|
||||||
|
@Path("id") scheduledStatusId: String
|
||||||
|
): Call<ResponseBody>
|
||||||
|
|
||||||
@GET("api/v1/accounts/verify_credentials")
|
@GET("api/v1/accounts/verify_credentials")
|
||||||
fun accountVerifyCredentials(): Single<Account>
|
fun accountVerifyCredentials(): Single<Account>
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,11 @@ package com.keylesspalace.tusky.receiver
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.app.RemoteInput
|
import androidx.core.app.RemoteInput
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import android.util.Log
|
|
||||||
import com.keylesspalace.tusky.ComposeActivity
|
import com.keylesspalace.tusky.ComposeActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
@ -92,6 +92,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
emptyList(),
|
emptyList(),
|
||||||
emptyList(),
|
emptyList(),
|
||||||
emptyList(),
|
emptyList(),
|
||||||
|
null,
|
||||||
citedStatusId,
|
citedStatusId,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
|
|
@ -18,6 +18,7 @@ import androidx.core.content.ContextCompat
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
@ -140,6 +141,7 @@ class SendTootService : Service(), Injectable {
|
||||||
tootToSend.visibility,
|
tootToSend.visibility,
|
||||||
tootToSend.sensitive,
|
tootToSend.sensitive,
|
||||||
tootToSend.mediaIds,
|
tootToSend.mediaIds,
|
||||||
|
tootToSend.scheduledAt,
|
||||||
tootToSend.poll
|
tootToSend.poll
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -156,6 +158,7 @@ class SendTootService : Service(), Injectable {
|
||||||
val callback = object : Callback<Status> {
|
val callback = object : Callback<Status> {
|
||||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||||
|
|
||||||
|
val scheduled = !tootToSend.scheduledAt.isNullOrEmpty()
|
||||||
tootsToSend.remove(tootId)
|
tootsToSend.remove(tootId)
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
|
@ -164,7 +167,11 @@ class SendTootService : Service(), Injectable {
|
||||||
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
|
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scheduled) {
|
||||||
|
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
|
||||||
|
} else {
|
||||||
response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
|
response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
|
||||||
|
}
|
||||||
|
|
||||||
notificationManager.cancel(tootId)
|
notificationManager.cancel(tootId)
|
||||||
|
|
||||||
|
@ -284,6 +291,7 @@ class SendTootService : Service(), Injectable {
|
||||||
mediaIds: List<String>,
|
mediaIds: List<String>,
|
||||||
mediaUris: List<Uri>,
|
mediaUris: List<Uri>,
|
||||||
mediaDescriptions: List<String>,
|
mediaDescriptions: List<String>,
|
||||||
|
scheduledAt: String?,
|
||||||
inReplyToId: String?,
|
inReplyToId: String?,
|
||||||
poll: NewPoll?,
|
poll: NewPoll?,
|
||||||
replyingStatusContent: String?,
|
replyingStatusContent: String?,
|
||||||
|
@ -303,6 +311,7 @@ class SendTootService : Service(), Injectable {
|
||||||
mediaIds,
|
mediaIds,
|
||||||
mediaUris.map { it.toString() },
|
mediaUris.map { it.toString() },
|
||||||
mediaDescriptions,
|
mediaDescriptions,
|
||||||
|
scheduledAt,
|
||||||
inReplyToId,
|
inReplyToId,
|
||||||
poll,
|
poll,
|
||||||
replyingStatusContent,
|
replyingStatusContent,
|
||||||
|
@ -346,6 +355,7 @@ data class TootToSend(val text: String,
|
||||||
val mediaIds: List<String>,
|
val mediaIds: List<String>,
|
||||||
val mediaUris: List<String>,
|
val mediaUris: List<String>,
|
||||||
val mediaDescriptions: List<String>,
|
val mediaDescriptions: List<String>,
|
||||||
|
val scheduledAt: String?,
|
||||||
val inReplyToId: String?,
|
val inReplyToId: String?,
|
||||||
val poll: NewPoll?,
|
val poll: NewPoll?,
|
||||||
val replyingStatusContent: String?,
|
val replyingStatusContent: String?,
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* Copyright 2019 kyori19
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.util;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class VersionUtils {
|
||||||
|
|
||||||
|
private int major;
|
||||||
|
private int minor;
|
||||||
|
private int patch;
|
||||||
|
|
||||||
|
public VersionUtils(String versionString) {
|
||||||
|
String regex = "([0-9]+)\\.([0-9]+)\\.([0-9]+).*";
|
||||||
|
Pattern pattern = Pattern.compile(regex);
|
||||||
|
Matcher matcher = pattern.matcher(versionString);
|
||||||
|
if (matcher.find()) {
|
||||||
|
major = Integer.parseInt(matcher.group(1));
|
||||||
|
minor = Integer.parseInt(matcher.group(2));
|
||||||
|
patch = Integer.parseInt(matcher.group(3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsScheduledToots() {
|
||||||
|
return (major == 2) ? ( (minor == 7) ? (patch >= 0) : (minor > 7) ) : (major > 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
/* Copyright 2019 kyori19
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.view;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
|
|
||||||
|
import com.google.android.material.datepicker.CalendarConstraints;
|
||||||
|
import com.google.android.material.datepicker.DateValidatorPointForward;
|
||||||
|
import com.google.android.material.datepicker.MaterialDatePicker;
|
||||||
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.fragment.TimePickerFragment;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
public class ComposeScheduleView extends ConstraintLayout {
|
||||||
|
|
||||||
|
private DateFormat dateFormat;
|
||||||
|
private DateFormat timeFormat;
|
||||||
|
private SimpleDateFormat iso8601;
|
||||||
|
|
||||||
|
private Button resetScheduleButton;
|
||||||
|
private TextView scheduledDateTimeView;
|
||||||
|
|
||||||
|
private Calendar scheduleDateTime;
|
||||||
|
|
||||||
|
public ComposeScheduleView(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComposeScheduleView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComposeScheduleView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
inflate(getContext(), R.layout.view_compose_schedule, this);
|
||||||
|
|
||||||
|
dateFormat = SimpleDateFormat.getDateInstance();
|
||||||
|
timeFormat = SimpleDateFormat.getTimeInstance();
|
||||||
|
iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
|
||||||
|
iso8601.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
|
||||||
|
resetScheduleButton = findViewById(R.id.resetScheduleButton);
|
||||||
|
scheduledDateTimeView = findViewById(R.id.scheduledDateTime);
|
||||||
|
|
||||||
|
scheduledDateTimeView.setOnClickListener(v -> openPickDateDialog());
|
||||||
|
|
||||||
|
scheduleDateTime = null;
|
||||||
|
|
||||||
|
setScheduledDateTime();
|
||||||
|
|
||||||
|
setEditIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setScheduledDateTime() {
|
||||||
|
if (scheduleDateTime == null) {
|
||||||
|
scheduledDateTimeView.setText(R.string.hint_configure_scheduled_toot);
|
||||||
|
} else {
|
||||||
|
scheduledDateTimeView.setText(String.format("%s %s",
|
||||||
|
dateFormat.format(scheduleDateTime.getTime()),
|
||||||
|
timeFormat.format(scheduleDateTime.getTime())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setEditIcons() {
|
||||||
|
final int size = scheduledDateTimeView.getLineHeight();
|
||||||
|
|
||||||
|
Drawable icon = getContext().getDrawable(R.drawable.ic_create_24dp);
|
||||||
|
if (icon == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
icon.setBounds(0, 0, size, size);
|
||||||
|
|
||||||
|
scheduledDateTimeView.setCompoundDrawables(null, null, icon, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResetOnClickListener(OnClickListener listener) {
|
||||||
|
resetScheduleButton.setOnClickListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetSchedule() {
|
||||||
|
scheduleDateTime = null;
|
||||||
|
setScheduledDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openPickDateDialog() {
|
||||||
|
long yesterday = Calendar.getInstance().getTimeInMillis() - 24 * 60 * 60 * 1000;
|
||||||
|
CalendarConstraints calendarConstraints = new CalendarConstraints.Builder()
|
||||||
|
.setValidator(new DateValidatorPointForward(yesterday))
|
||||||
|
.build();
|
||||||
|
if (scheduleDateTime == null) {
|
||||||
|
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault());
|
||||||
|
}
|
||||||
|
MaterialDatePicker<Long> picker = MaterialDatePicker.Builder
|
||||||
|
.datePicker()
|
||||||
|
.setSelection(scheduleDateTime.getTimeInMillis())
|
||||||
|
.setCalendarConstraints(calendarConstraints)
|
||||||
|
.build();
|
||||||
|
picker.addOnPositiveButtonClickListener(this::onDateSet);
|
||||||
|
picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "date_picker");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openPickTimeDialog() {
|
||||||
|
TimePickerFragment picker = new TimePickerFragment();
|
||||||
|
if (scheduleDateTime != null) {
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putInt(TimePickerFragment.PICKER_TIME_HOUR, scheduleDateTime.get(Calendar.HOUR_OF_DAY));
|
||||||
|
args.putInt(TimePickerFragment.PICKER_TIME_MINUTE, scheduleDateTime.get(Calendar.MINUTE));
|
||||||
|
picker.setArguments(args);
|
||||||
|
}
|
||||||
|
picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDateTime(String scheduledAt) {
|
||||||
|
Date date;
|
||||||
|
try {
|
||||||
|
date = iso8601.parse(scheduledAt);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scheduleDateTime == null) {
|
||||||
|
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault());
|
||||||
|
}
|
||||||
|
scheduleDateTime.setTime(date);
|
||||||
|
setScheduledDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onDateSet(long selection) {
|
||||||
|
if (scheduleDateTime == null) {
|
||||||
|
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault());
|
||||||
|
}
|
||||||
|
Calendar newDate = Calendar.getInstance(TimeZone.getDefault());
|
||||||
|
newDate.setTimeInMillis(selection);
|
||||||
|
scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE));
|
||||||
|
openPickTimeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onTimeSet(int hourOfDay, int minute) {
|
||||||
|
if (scheduleDateTime == null) {
|
||||||
|
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault());
|
||||||
|
}
|
||||||
|
scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
|
||||||
|
scheduleDateTime.set(Calendar.MINUTE, minute);
|
||||||
|
setScheduledDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTime() {
|
||||||
|
if (scheduleDateTime == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return iso8601.format(scheduleDateTime.getTime());
|
||||||
|
}
|
||||||
|
}
|
9
app/src/main/res/drawable-anydpi/ic_access_time.xml
Normal file
9
app/src/main/res/drawable-anydpi/ic_access_time.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
|
||||||
|
</vector>
|
|
@ -231,6 +231,20 @@
|
||||||
app:behavior_peekHeight="0dp"
|
app:behavior_peekHeight="0dp"
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
||||||
|
|
||||||
|
<com.keylesspalace.tusky.view.ComposeScheduleView
|
||||||
|
android:id="@+id/composeScheduleView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
android:elevation="12dp"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="52dp"
|
||||||
|
app:behavior_hideable="true"
|
||||||
|
app:behavior_peekHeight="0dp"
|
||||||
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -299,6 +313,17 @@
|
||||||
android:tooltipText="@string/action_emoji_keyboard"
|
android:tooltipText="@string/action_emoji_keyboard"
|
||||||
app:srcCompat="@drawable/ic_emoji_24dp" />
|
app:srcCompat="@drawable/ic_emoji_24dp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composeScheduleButton"
|
||||||
|
style="?attr/image_button_style"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:contentDescription="@string/action_schedule_toot"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:tooltipText="@string/action_schedule_toot"
|
||||||
|
app:srcCompat="@drawable/ic_access_time" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
53
app/src/main/res/layout/activity_scheduled_toot.xml
Normal file
53
app/src/main/res/layout/activity_scheduled_toot.xml
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/activity_view_thread"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="com.keylesspalace.tusky.AccountListActivity">
|
||||||
|
|
||||||
|
<include layout="@layout/toolbar_basic" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_bar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.keylesspalace.tusky.view.BackgroundMessageView
|
||||||
|
android:id="@+id/errorMessageView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@android:color/transparent"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@drawable/elephant_error"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/swipe_refresh_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/scheduled_toot_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
40
app/src/main/res/layout/item_scheduled_toot.xml
Normal file
40
app/src/main/res/layout/item_scheduled_toot.xml
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="0.91"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:textSize="?attr/status_text_medium" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/edit"
|
||||||
|
style="?attr/image_button_style"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_margin="12dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/action_edit"
|
||||||
|
android:padding="4dp"
|
||||||
|
app:srcCompat="@drawable/ic_create_24dp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/delete"
|
||||||
|
style="?attr/image_button_style"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_margin="12dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/action_delete"
|
||||||
|
android:padding="4dp"
|
||||||
|
app:srcCompat="@drawable/ic_clear_24dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
31
app/src/main/res/layout/view_compose_schedule.xml
Normal file
31
app/src/main/res/layout/view_compose_schedule.xml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/resetScheduleButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:text="@string/action_reset_schedule"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/scheduledDateTime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:textColor="?android:textColorTertiary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:text="2020/01/01 00:00:00" />
|
||||||
|
|
||||||
|
</merge>
|
|
@ -40,6 +40,7 @@
|
||||||
<string name="title_follow_requests">Follow Requests</string>
|
<string name="title_follow_requests">Follow Requests</string>
|
||||||
<string name="title_edit_profile">Edit your profile</string>
|
<string name="title_edit_profile">Edit your profile</string>
|
||||||
<string name="title_saved_toot">Drafts</string>
|
<string name="title_saved_toot">Drafts</string>
|
||||||
|
<string name="title_scheduled_toot">Scheduled toots</string>
|
||||||
<string name="title_licenses">Licenses</string>
|
<string name="title_licenses">Licenses</string>
|
||||||
|
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
|
@ -80,6 +81,7 @@
|
||||||
<string name="action_hide_reblogs">Hide boosts</string>
|
<string name="action_hide_reblogs">Hide boosts</string>
|
||||||
<string name="action_show_reblogs">Show boosts</string>
|
<string name="action_show_reblogs">Show boosts</string>
|
||||||
<string name="action_report">Report</string>
|
<string name="action_report">Report</string>
|
||||||
|
<string name="action_edit">Edit</string>
|
||||||
<string name="action_delete">Delete</string>
|
<string name="action_delete">Delete</string>
|
||||||
<string name="action_delete_and_redraft">Delete and re-draft</string>
|
<string name="action_delete_and_redraft">Delete and re-draft</string>
|
||||||
<string name="action_send">TOOT</string>
|
<string name="action_send">TOOT</string>
|
||||||
|
@ -114,9 +116,12 @@
|
||||||
<string name="action_reject">Reject</string>
|
<string name="action_reject">Reject</string>
|
||||||
<string name="action_search">Search</string>
|
<string name="action_search">Search</string>
|
||||||
<string name="action_access_saved_toot">Drafts</string>
|
<string name="action_access_saved_toot">Drafts</string>
|
||||||
|
<string name="action_access_scheduled_toot">Scheduled toots</string>
|
||||||
<string name="action_toggle_visibility">Toot visibility</string>
|
<string name="action_toggle_visibility">Toot visibility</string>
|
||||||
<string name="action_content_warning">Content warning</string>
|
<string name="action_content_warning">Content warning</string>
|
||||||
<string name="action_emoji_keyboard">Emoji keyboard</string>
|
<string name="action_emoji_keyboard">Emoji keyboard</string>
|
||||||
|
<string name="action_schedule_toot">Schedule Toot</string>
|
||||||
|
<string name="action_reset_schedule">Reset</string>
|
||||||
<string name="action_add_tab">Add Tab</string>
|
<string name="action_add_tab">Add Tab</string>
|
||||||
<string name="action_links">Links</string>
|
<string name="action_links">Links</string>
|
||||||
<string name="action_mentions">Mentions</string>
|
<string name="action_mentions">Mentions</string>
|
||||||
|
@ -152,6 +157,7 @@
|
||||||
|
|
||||||
<string name="hint_domain">Which instance?</string>
|
<string name="hint_domain">Which instance?</string>
|
||||||
<string name="hint_compose">What\'s happening?</string>
|
<string name="hint_compose">What\'s happening?</string>
|
||||||
|
<string name="hint_configure_scheduled_toot">Tap here to configure scheduled toot.</string>
|
||||||
<string name="hint_content_warning">Content warning</string>
|
<string name="hint_content_warning">Content warning</string>
|
||||||
<string name="hint_display_name">Display name</string>
|
<string name="hint_display_name">Display name</string>
|
||||||
<string name="hint_note">Bio</string>
|
<string name="hint_note">Bio</string>
|
||||||
|
|
Loading…
Reference in a new issue