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.recyclerview:recyclerview: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.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.1.0-alpha04'
|
||||
|
|
|
@ -135,6 +135,7 @@
|
|||
android:name=".components.report.ReportActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity android:name=".components.instancemute.InstanceListActivity" />
|
||||
<activity android:name=".ScheduledTootActivity" />
|
||||
|
||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||
<receiver
|
||||
|
|
|
@ -17,7 +17,9 @@ package com.keylesspalace.tusky;
|
|||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.DatePickerDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.app.TimePickerDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
|
@ -55,14 +57,35 @@ import android.view.Window;
|
|||
import android.view.WindowManager;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Button;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TimePicker;
|
||||
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.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
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.StringUtils;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.keylesspalace.tusky.util.VersionUtils;
|
||||
import com.keylesspalace.tusky.view.AddPollDialog;
|
||||
import com.keylesspalace.tusky.view.ComposeOptionsListener;
|
||||
import com.keylesspalace.tusky.view.ComposeOptionsView;
|
||||
import com.keylesspalace.tusky.view.ComposeScheduleView;
|
||||
import com.keylesspalace.tusky.view.EditTextTyped;
|
||||
import com.keylesspalace.tusky.view.PollPreviewView;
|
||||
import com.keylesspalace.tusky.view.ProgressImageView;
|
||||
|
@ -123,25 +148,6 @@ import java.util.concurrent.CountDownLatch;
|
|||
|
||||
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 io.reactivex.Single;
|
||||
import io.reactivex.SingleObserver;
|
||||
|
@ -169,7 +175,8 @@ public final class ComposeActivity
|
|||
implements ComposeOptionsListener,
|
||||
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||
OnEmojiSelectedListener,
|
||||
Injectable, InputConnectionCompat.OnCommitContentListener {
|
||||
Injectable, InputConnectionCompat.OnCommitContentListener,
|
||||
TimePickerDialog.OnTimeSetListener {
|
||||
|
||||
private static final String TAG = "ComposeActivity"; // logging tag
|
||||
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_CONTENT_EXTRA = "replying_status_content";
|
||||
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 POLL_EXTRA = "poll";
|
||||
// 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 emojiButton;
|
||||
private ImageButton hideMediaToggle;
|
||||
private ImageButton scheduleButton;
|
||||
private TextView actionAddPoll;
|
||||
private Button atButton;
|
||||
private Button hashButton;
|
||||
|
@ -225,6 +234,8 @@ public final class ComposeActivity
|
|||
private BottomSheetBehavior composeOptionsBehavior;
|
||||
private BottomSheetBehavior addMediaBehavior;
|
||||
private BottomSheetBehavior emojiBehavior;
|
||||
private BottomSheetBehavior scheduleBehavior;
|
||||
private ComposeScheduleView scheduleView;
|
||||
private RecyclerView emojiView;
|
||||
|
||||
private PollPreviewView pollPreview;
|
||||
|
@ -278,6 +289,8 @@ public final class ComposeActivity
|
|||
contentWarningButton = findViewById(R.id.composeContentWarningButton);
|
||||
emojiButton = findViewById(R.id.composeEmojiButton);
|
||||
hideMediaToggle = findViewById(R.id.composeHideMediaButton);
|
||||
scheduleButton = findViewById(R.id.composeScheduleButton);
|
||||
scheduleView = findViewById(R.id.composeScheduleView);
|
||||
emojiView = findViewById(R.id.emojiView);
|
||||
emojiList = Collections.emptyList();
|
||||
atButton = findViewById(R.id.atButton);
|
||||
|
@ -361,6 +374,8 @@ public final class ComposeActivity
|
|||
|
||||
addMediaBehavior = BottomSheetBehavior.from(findViewById(R.id.addMediaBottomSheet));
|
||||
|
||||
scheduleBehavior = BottomSheetBehavior.from(scheduleView);
|
||||
|
||||
emojiBehavior = BottomSheetBehavior.from(emojiView);
|
||||
|
||||
emojiView.setLayoutManager(new GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false));
|
||||
|
@ -374,6 +389,8 @@ public final class ComposeActivity
|
|||
contentWarningButton.setOnClickListener(v -> onContentWarningChanged());
|
||||
emojiButton.setOnClickListener(v -> showEmojis());
|
||||
hideMediaToggle.setOnClickListener(v -> toggleHideMedia());
|
||||
scheduleButton.setOnClickListener(v -> showScheduleView());
|
||||
scheduleView.setResetOnClickListener(v -> resetSchedule());
|
||||
atButton.setOnClickListener(v -> atButtonClicked());
|
||||
hashButton.setOnClickListener(v -> hashButtonClicked());
|
||||
|
||||
|
@ -521,6 +538,11 @@ public final class ComposeActivity
|
|||
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);
|
||||
|
||||
if(intent.hasExtra(POLL_EXTRA) && (mediaAttachments == null || mediaAttachments.size() == 0)) {
|
||||
|
@ -536,6 +558,7 @@ public final class ComposeActivity
|
|||
setStatusVisibility(startingVisibility);
|
||||
|
||||
updateHideMediaToggle();
|
||||
updateScheduleButton();
|
||||
updateVisibleCharactersLeft();
|
||||
|
||||
// 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() {
|
||||
pickButton.setClickable(false);
|
||||
visibilityButton.setClickable(false);
|
||||
emojiButton.setClickable(false);
|
||||
hideMediaToggle.setClickable(false);
|
||||
scheduleButton.setClickable(false);
|
||||
tootButton.setEnabled(false);
|
||||
}
|
||||
|
||||
|
@ -812,6 +846,7 @@ public final class ComposeActivity
|
|||
visibilityButton.setClickable(true);
|
||||
emojiButton.setClickable(true);
|
||||
hideMediaToggle.setClickable(true);
|
||||
scheduleButton.setClickable(true);
|
||||
tootButton.setEnabled(true);
|
||||
}
|
||||
|
||||
|
@ -859,12 +894,23 @@ public final class ComposeActivity
|
|||
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
|
||||
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
} else {
|
||||
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() {
|
||||
|
||||
if (emojiView.getAdapter() != null) {
|
||||
|
@ -876,7 +922,7 @@ public final class ComposeActivity
|
|||
emojiBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
|
||||
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
} else {
|
||||
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
}
|
||||
|
@ -891,7 +937,7 @@ public final class ComposeActivity
|
|||
addMediaBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
|
||||
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
} else {
|
||||
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
}
|
||||
|
@ -1084,7 +1130,8 @@ public final class ComposeActivity
|
|||
}
|
||||
|
||||
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_AUTHOR_USERNAME_EXTRA),
|
||||
getIntent().getStringExtra(SAVED_JSON_URLS_EXTRA),
|
||||
|
@ -1744,10 +1791,12 @@ public final class ComposeActivity
|
|||
// Acting like a teen: deliberately ignoring parent.
|
||||
if (composeOptionsBehavior.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);
|
||||
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1947,6 +1996,10 @@ public final class ComposeActivity
|
|||
updateVisibleCharactersLeft();
|
||||
}
|
||||
|
||||
if (!new VersionUtils(instance.getVersion()).supportsScheduledToots()) {
|
||||
scheduleButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (instance.getPollLimits() != null) {
|
||||
maxPollOptions = instance.getPollLimits().getMaxOptions();
|
||||
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 {
|
||||
@Nullable
|
||||
private Integer savedTootUid;
|
||||
|
@ -2074,6 +2140,8 @@ public final class ComposeActivity
|
|||
@Nullable
|
||||
private ArrayList<Attachment> mediaAttachments;
|
||||
@Nullable
|
||||
private String scheduledAt;
|
||||
@Nullable
|
||||
private Boolean sensitive;
|
||||
@Nullable
|
||||
private NewPoll poll;
|
||||
|
@ -2138,6 +2206,11 @@ public final class ComposeActivity
|
|||
return this;
|
||||
}
|
||||
|
||||
public IntentBuilder scheduledAt(String scheduledAt) {
|
||||
this.scheduledAt = scheduledAt;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IntentBuilder sensitive(boolean sensitive) {
|
||||
this.sensitive = sensitive;
|
||||
return this;
|
||||
|
@ -2188,6 +2261,9 @@ public final class ComposeActivity
|
|||
if (mediaAttachments != null) {
|
||||
intent.putParcelableArrayListExtra(MEDIA_ATTACHMENTS_EXTRA, mediaAttachments);
|
||||
}
|
||||
if (scheduledAt != null) {
|
||||
intent.putExtra(SCHEDULED_AT_EXTRA, scheduledAt);
|
||||
}
|
||||
if (sensitive != null) {
|
||||
intent.putExtra(SENSITIVE_EXTRA, sensitive);
|
||||
}
|
||||
|
|
|
@ -15,26 +15,11 @@
|
|||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
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.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
@ -42,6 +27,17 @@ import android.view.KeyEvent;
|
|||
import android.widget.ImageButton;
|
||||
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.EventHub;
|
||||
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_LOG_OUT = 8;
|
||||
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";
|
||||
|
||||
@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_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_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 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));
|
||||
|
@ -433,6 +431,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
} else if (drawerItemIdentifier == DRAWER_ITEM_SAVED_TOOT) {
|
||||
Intent intent = new Intent(MainActivity.this, SavedTootActivity.class);
|
||||
startActivityWithSlideInAnimation(intent);
|
||||
} else if (drawerItemIdentifier == DRAWER_ITEM_SCHEDULED_TOOT) {
|
||||
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(this));
|
||||
} else if (drawerItemIdentifier == DRAWER_ITEM_LISTS) {
|
||||
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 StatusDeletedEvent(val statusId: String) : Dispatchable
|
||||
data class StatusComposedEvent(val status: Status) : Dispatchable
|
||||
data class StatusScheduledEvent(val status: Status) : Dispatchable
|
||||
data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
|
||||
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
||||
|
|
|
@ -97,4 +97,7 @@ abstract class ActivitiesModule {
|
|||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesScheduledTootActivity(): ScheduledTootActivity
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ data class NewStatus(
|
|||
val visibility: String,
|
||||
val sensitive: Boolean,
|
||||
@SerializedName("media_ids") val mediaIds: List<String>?,
|
||||
@SerializedName("scheduled_at") val scheduledAt: String?,
|
||||
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 retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.*
|
||||
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/
|
||||
|
@ -202,6 +190,14 @@ interface MastodonApi {
|
|||
@Path("id") statusId: String
|
||||
): 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")
|
||||
fun accountVerifyCredentials(): Single<Account>
|
||||
|
||||
|
|
|
@ -18,11 +18,11 @@ package com.keylesspalace.tusky.receiver
|
|||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.content.ContextCompat
|
||||
import android.util.Log
|
||||
import com.keylesspalace.tusky.ComposeActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
|
@ -92,6 +92,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
|||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
null,
|
||||
citedStatusId,
|
||||
null,
|
||||
null,
|
||||
|
|
|
@ -18,6 +18,7 @@ import androidx.core.content.ContextCompat
|
|||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
|
@ -140,6 +141,7 @@ class SendTootService : Service(), Injectable {
|
|||
tootToSend.visibility,
|
||||
tootToSend.sensitive,
|
||||
tootToSend.mediaIds,
|
||||
tootToSend.scheduledAt,
|
||||
tootToSend.poll
|
||||
)
|
||||
|
||||
|
@ -156,6 +158,7 @@ class SendTootService : Service(), Injectable {
|
|||
val callback = object : Callback<Status> {
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
|
||||
val scheduled = !tootToSend.scheduledAt.isNullOrEmpty()
|
||||
tootsToSend.remove(tootId)
|
||||
|
||||
if (response.isSuccessful) {
|
||||
|
@ -164,7 +167,11 @@ class SendTootService : Service(), Injectable {
|
|||
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
|
||||
}
|
||||
|
||||
response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
|
||||
if (scheduled) {
|
||||
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
|
||||
} else {
|
||||
response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
|
||||
}
|
||||
|
||||
notificationManager.cancel(tootId)
|
||||
|
||||
|
@ -284,6 +291,7 @@ class SendTootService : Service(), Injectable {
|
|||
mediaIds: List<String>,
|
||||
mediaUris: List<Uri>,
|
||||
mediaDescriptions: List<String>,
|
||||
scheduledAt: String?,
|
||||
inReplyToId: String?,
|
||||
poll: NewPoll?,
|
||||
replyingStatusContent: String?,
|
||||
|
@ -303,6 +311,7 @@ class SendTootService : Service(), Injectable {
|
|||
mediaIds,
|
||||
mediaUris.map { it.toString() },
|
||||
mediaDescriptions,
|
||||
scheduledAt,
|
||||
inReplyToId,
|
||||
poll,
|
||||
replyingStatusContent,
|
||||
|
@ -346,6 +355,7 @@ data class TootToSend(val text: String,
|
|||
val mediaIds: List<String>,
|
||||
val mediaUris: List<String>,
|
||||
val mediaDescriptions: List<String>,
|
||||
val scheduledAt: String?,
|
||||
val inReplyToId: String?,
|
||||
val poll: NewPoll?,
|
||||
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: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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -299,6 +313,17 @@
|
|||
android:tooltipText="@string/action_emoji_keyboard"
|
||||
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
|
||||
android:layout_width="0dp"
|
||||
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_edit_profile">Edit your profile</string>
|
||||
<string name="title_saved_toot">Drafts</string>
|
||||
<string name="title_scheduled_toot">Scheduled toots</string>
|
||||
<string name="title_licenses">Licenses</string>
|
||||
|
||||
<string name="status_username_format">\@%s</string>
|
||||
|
@ -80,6 +81,7 @@
|
|||
<string name="action_hide_reblogs">Hide boosts</string>
|
||||
<string name="action_show_reblogs">Show boosts</string>
|
||||
<string name="action_report">Report</string>
|
||||
<string name="action_edit">Edit</string>
|
||||
<string name="action_delete">Delete</string>
|
||||
<string name="action_delete_and_redraft">Delete and re-draft</string>
|
||||
<string name="action_send">TOOT</string>
|
||||
|
@ -114,9 +116,12 @@
|
|||
<string name="action_reject">Reject</string>
|
||||
<string name="action_search">Search</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_content_warning">Content warning</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_links">Links</string>
|
||||
<string name="action_mentions">Mentions</string>
|
||||
|
@ -152,6 +157,7 @@
|
|||
|
||||
<string name="hint_domain">Which instance?</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_display_name">Display name</string>
|
||||
<string name="hint_note">Bio</string>
|
||||
|
|
Loading…
Reference in a new issue