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:
kyori19 2019-10-03 04:28:12 +09:00 committed by Konrad Pozniak
parent a6b9d2f67e
commit 9e4c19a47e
23 changed files with 933 additions and 56 deletions

View File

@ -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'

View File

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

View File

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

View File

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

View File

@ -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) {
}
})
}
}

View File

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

View File

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

View File

@ -97,4 +97,7 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesInstanceListActivity(): InstanceListActivity
@ContributesAndroidInjector
abstract fun contributesScheduledTootActivity(): ScheduledTootActivity
}

View File

@ -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?
)

View File

@ -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>
)

View File

@ -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?
)

View File

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

View File

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

View File

@ -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,

View File

@ -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?,

View File

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

View File

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

View 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>

View File

@ -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"

View 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>

View 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>

View 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>

View File

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