version upgrade, added status schedule support

This commit is contained in:
nuclearfog 2023-09-21 22:42:22 +02:00
parent 5b22305713
commit 8a05fa79f6
No known key found for this signature in database
GPG Key ID: 03488A185C476379
15 changed files with 245 additions and 41 deletions

View File

@ -11,8 +11,8 @@ android {
applicationId 'org.nuclearfog.twidda'
minSdkVersion 21
targetSdkVersion 34
versionCode 98
versionName '3.4.2'
versionCode 99
versionName '3.4.3'
resConfigs 'en', 'es', 'de-rDE', 'zh-rCN'
}

View File

@ -1,5 +1,7 @@
package org.nuclearfog.twidda.backend.api;
import androidx.annotation.Nullable;
import org.nuclearfog.twidda.backend.helper.ConnectionResult;
import org.nuclearfog.twidda.backend.helper.MediaStatus;
import org.nuclearfog.twidda.backend.helper.update.ConnectionUpdate;
@ -475,6 +477,7 @@ public interface Connection {
* @param mediaIds IDs of the uploaded media files if any
* @return uploaded status
*/
@Nullable
Status updateStatus(StatusUpdate update, List<Long> mediaIds) throws ConnectionException;
/**

View File

@ -1,5 +1,6 @@
package org.nuclearfog.twidda.backend.api.mastodon;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Base64;
@ -70,9 +71,12 @@ import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECPoint;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
@ -148,6 +152,9 @@ public class Mastodon implements Connection {
private static final String ENDPOINT_FILTER = "/api/v2/filters";
private static final String ENDPOINT_REPORT = "/api/v1/reports";
@SuppressLint("SimpleDateFormat")
private static final DateFormat ISO_8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm");
private static final MediaType TYPE_TEXT = MediaType.parse("text/plain");
private static final MediaType TYPE_STREAM = MediaType.parse("application/octet-stream");
@ -641,6 +648,7 @@ public class Mastodon implements Connection {
@Override
@Nullable
public Status updateStatus(StatusUpdate update, List<Long> mediaIds) throws MastodonException {
List<String> params = new ArrayList<>();
// add identifier to prevent duplicate posts
@ -657,7 +665,7 @@ public class Mastodon implements Connection {
if (update.getReplyId() != 0L)
params.add("in_reply_to_id=" + update.getReplyId());
if (update.getScheduleTime() != 0L)
params.add("scheduled_at=" + update.getScheduleTime());
params.add("scheduled_at=" + ISO_8601_FORMAT.format(new Date(update.getScheduleTime())));
if (update.getVisibility() == Status.VISIBLE_DIRECT)
params.add("visibility=direct");
else if (update.getVisibility() == Status.VISIBLE_PRIVATE)
@ -701,7 +709,9 @@ public class Mastodon implements Connection {
else
response = post(ENDPOINT_STATUS, params);
if (response.code() == 200) {
return createStatus(response);
if (update.getScheduleTime() == 0L)
return createStatus(response);
return null; // when scheduling, ScheduledStatus will be returned from API instead, which is not compatible to Status
}
throw new MastodonException(response);
} catch (IOException e) {

View File

@ -187,7 +187,7 @@ public class StatusUpdate implements Serializable, Closeable {
public void addPoll(PollUpdate poll) {
if (mediaStatuses.isEmpty()) {
this.poll = poll;
attachmentLimitReached = true;
attachmentLimitReached = poll != null;
}
}

View File

@ -428,8 +428,14 @@ public class StatusEditor extends MediaActivity implements ActivityResultCallbac
@Override
public void onPollUpdate(@Nullable PollUpdate update) {
statusUpdate.addPoll(update);
if (statusUpdate.mediaLimitReached()) {
mediaBtn.setVisibility(View.GONE);
if (update != null) {
if (statusUpdate.mediaLimitReached()) {
mediaBtn.setVisibility(View.GONE);
}
} else {
if (!statusUpdate.mediaLimitReached()) {
mediaBtn.setVisibility(View.VISIBLE);
}
}
}

View File

@ -59,21 +59,6 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
*/
public static final int STATUS_EDITOR_ERROR = 609;
/**
* show dialog to delete message
*/
public static final int MESSAGE_DELETE = 610;
/**
* show dialog to discard message
*/
public static final int MESSAGE_EDITOR_LEAVE = 611;
/**
* show dialog if an error occurs while uploading a message
*/
public static final int MESSAGE_EDITOR_ERROR = 612;
/**
* show "discard profile changes" dialog
*/
@ -205,10 +190,6 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
int cancelIconRes = R.drawable.cross;
// override values depending on type
switch (type) {
case MESSAGE_DELETE:
messageRes = R.string.confirm_delete_message;
break;
case WRONG_PROXY:
titleVis = View.VISIBLE;
messageRes = R.string.error_wrong_connection_settings;
@ -231,12 +212,7 @@ public class ConfirmDialog extends Dialog implements OnClickListener {
messageRes = R.string.confirm_cancel_status;
break;
case MESSAGE_EDITOR_LEAVE:
messageRes = R.string.confirm_cancel_message;
break;
case LIST_EDITOR_ERROR:
case MESSAGE_EDITOR_ERROR:
case STATUS_EDITOR_ERROR:
case PROFILE_EDITOR_ERROR:
titleVis = View.VISIBLE;

View File

@ -4,9 +4,11 @@ import android.app.Activity;
import android.app.Dialog;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Spinner;
@ -29,12 +31,13 @@ import java.util.TreeMap;
*
* @author nuclearfog
*/
public class StatusPreferenceDialog extends Dialog implements OnCheckedChangeListener, OnItemSelectedListener {
public class StatusPreferenceDialog extends Dialog implements OnCheckedChangeListener, OnItemSelectedListener, OnClickListener, TimePickerDialog.TimeSelectedCallback {
private Spinner visibilitySelector, languageSelector;
private SwitchButton sensitiveCheck, spoilerCheck;
private DropdownAdapter visibility_adapter, language_adapter;
private TimePickerDialog timePicker;
private GlobalSettings settings;
private StatusUpdate statusUpdate;
private String[] languageCodes;
@ -47,6 +50,8 @@ public class StatusPreferenceDialog extends Dialog implements OnCheckedChangeLis
this.statusUpdate = statusUpdate;
visibility_adapter = new DropdownAdapter(activity.getApplicationContext());
language_adapter = new DropdownAdapter(activity.getApplicationContext());
timePicker = new TimePickerDialog(activity, this);
settings = GlobalSettings.get(getContext());
// initialize language selector
@ -68,6 +73,7 @@ public class StatusPreferenceDialog extends Dialog implements OnCheckedChangeLis
ViewGroup rootView = findViewById(R.id.dialog_status_root);
View statusVisibility = findViewById(R.id.dialog_status_visibility_container);
View statusSpoiler = findViewById(R.id.dialog_status_spoiler_container);
Button timePicker = findViewById(R.id.dialog_status_time_picker);
languageSelector = findViewById(R.id.dialog_status_language);
visibilitySelector = findViewById(R.id.dialog_status_visibility);
sensitiveCheck = findViewById(R.id.dialog_status_sensitive);
@ -93,6 +99,7 @@ public class StatusPreferenceDialog extends Dialog implements OnCheckedChangeLis
spoilerCheck.setOnCheckedChangeListener(this);
languageSelector.setOnItemSelectedListener(this);
visibilitySelector.setOnItemSelectedListener(this);
timePicker.setOnClickListener(this);
}
@ -136,6 +143,14 @@ public class StatusPreferenceDialog extends Dialog implements OnCheckedChangeLis
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.dialog_status_time_picker) {
timePicker.show();
}
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView.getId() == R.id.dialog_status_sensitive) {
@ -177,4 +192,10 @@ public class StatusPreferenceDialog extends Dialog implements OnCheckedChangeLis
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
@Override
public void onTimeSelected(long time) {
statusUpdate.setScheduleTime(time);
}
}

View File

@ -0,0 +1,108 @@
package org.nuclearfog.twidda.ui.dialogs;
import android.app.Activity;
import android.app.Dialog;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.TimePicker;
import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.config.GlobalSettings;
import java.util.Date;
import java.util.GregorianCalendar;
/**
* Dialog used to show a date and time picker
*
* @author nuclearfog
*/
public class TimePickerDialog extends Dialog implements OnClickListener {
private TimePicker timePicker;
private DatePicker datePicker;
private TimeSelectedCallback callback;
/**
* @param callback callback used to set selected date
*/
public TimePickerDialog(Activity activity, TimeSelectedCallback callback) {
super(activity, R.style.DefaultDialog);
this.callback = callback;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dialog_timepicker);
datePicker = findViewById(R.id.dialog_timepicker_date);
timePicker = findViewById(R.id.dialog_timepicker_time);
ViewGroup root = findViewById(R.id.dialog_timepicker_root);
Button confirm = findViewById(R.id.dialog_timepicker_confirm);
Button cancel = findViewById(R.id.dialog_timepicker_remove);
GlobalSettings settings = GlobalSettings.get(getContext());
AppStyles.setTheme(root, settings.getPopupColor());
confirm.setOnClickListener(this);
cancel.setOnClickListener(this);
}
@Override
public void show() {
if (!isShowing()) {
super.show();
}
}
@Override
public void dismiss() {
super.dismiss();
datePicker.setVisibility(View.VISIBLE);
timePicker.setVisibility(View.INVISIBLE);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.dialog_timepicker_confirm) {
if (timePicker.getVisibility() == View.INVISIBLE) {
datePicker.setVisibility(View.INVISIBLE);
timePicker.setVisibility(View.VISIBLE);
} else {
GregorianCalendar calendar = new GregorianCalendar(datePicker.getYear(), datePicker.getMonth(),
datePicker.getDayOfMonth(), timePicker.getCurrentHour(), timePicker.getCurrentMinute());
Date selectedDate = calendar.getTime();
callback.onTimeSelected(selectedDate.getTime());
if (isShowing())
dismiss();
}
} else if (v.getId() == R.id.dialog_timepicker_remove) {
callback.onTimeSelected(0L);
if (isShowing())
dismiss();
}
}
/**
* Callback used to set selected date
*/
public interface TimeSelectedCallback {
/**
* set selected date time
*
* @param time selected date time or '0' if cancelled
*/
void onTimeSelected(long time);
}
}

View File

@ -88,7 +88,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
android:gravity="center_vertical"
android:layout_marginBottom="@dimen/dialog_status_layout_margins">
<TextView
android:layout_width="0dp"
@ -108,4 +109,30 @@
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<Button
android:id="@+id/dialog_status_time_picker"
android:layout_width="wrap_content"
android:layout_height="@dimen/dialog_status_button_height"
android:lines="1"
android:text="@string/dialog_status_schedule_set"
android:padding="@dimen/dialog_status_button_padding"
style="@style/FeedbackButton"/>
<TextView
android:id="@+id/dialog_status_time_set"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:lines="1"
android:layout_marginStart="@dimen/dialog_status_layout_margins"
android:layout_marginEnd="@dimen/dialog_status_layout_margins" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialog_timepicker_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<DatePicker
android:id="@+id/dialog_timepicker_date"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/dialog_timepicker_layout_margins"/>
<TimePicker
android:id="@+id/dialog_timepicker_time"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"
android:layout_margin="@dimen/dialog_timepicker_layout_margins"/>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end">
<Button
android:id="@+id/dialog_timepicker_remove"
android:layout_width="wrap_content"
android:layout_height="@dimen/dialog_timepicker_button_height"
android:text="@android:string/cancel"
android:layout_margin="@dimen/dialog_timepicker_layout_margins"
android:padding="@dimen/dialog_timepicker_button_padding"
style="@style/FeedbackButton"/>
<Button
android:id="@+id/dialog_timepicker_confirm"
android:layout_width="wrap_content"
android:layout_height="@dimen/dialog_timepicker_button_height"
android:text="@android:string/ok"
android:layout_margin="@dimen/dialog_timepicker_layout_margins"
android:padding="@dimen/dialog_timepicker_button_padding"
style="@style/FeedbackButton"/>
</LinearLayout>
</LinearLayout>

View File

@ -54,7 +54,6 @@
<string name="info_user_muted">stummgeschaltet!</string>
<string name="info_user_unmuted">Stummschaltung aufgehoben!</string>
<string name="username">Username</string>
<string name="confirm_cancel_message">Nachricht verwerfen?</string>
<string name="error_empty_status">Status leer!</string>
<string name="settings_logout">ausloggen</string>
<string name="confirm_cancel_status">Status verwerfen?</string>
@ -117,7 +116,6 @@
<string name="error_no_media_app">Keine Galerie App gefunden!</string>
<string name="delete_list">Liste löschen</string>
<string name="confirm_delete_list">Liste löschen?</string>
<string name="confirm_delete_message">Nachricht löschen?</string>
<string name="confirm_unfollow_list">Liste entfolgen?</string>
<string name="trend_range">\u0020Posts</string>
<string name="error_connection_failed">Verbindung fehlgeschlagen!</string>

View File

@ -188,7 +188,6 @@
<string name="confirm_delete_database">¿Limpiar datos de la app?</string>
<string name="status_sent_from">"Enviado desde: "</string>
<string name="username">Nombre de usuario</string>
<string name="confirm_cancel_message">¿Descartar mensaje?</string>
<string name="settings_logout">Cerrar sesión</string>
<string name="confirm_unfollow">¿No seguir a usuario?</string>
<string name="confirm_block">¿Bloquear a usuario?</string>
@ -230,7 +229,6 @@
<string name="edit_description_hint">Ingresar descripción del perfil</string>
<string name="page_profile_editor">Editar perfil</string>
<string name="confirm_delete_list">¿Borrar lista de usuario?</string>
<string name="confirm_delete_message">¿Borrar mensaje?</string>
<string name="confirm_unfollow_list">¿No seguir lista?</string>
<string name="trend_range">\u0020Publicaciones</string>
<string name="enter_username">Ingresar nombre de usuario</string>

View File

@ -22,7 +22,6 @@
<string name="confirm_delete_database">清除应用数据?</string>
<string name="status_sent_from">"来自:"</string>
<string name="username">用户名</string>
<string name="confirm_cancel_message">取消发送消息?</string>
<string name="settings_logout">登出</string>
<string name="confirm_unfollow">取消关注该用户?</string>
<string name="confirm_block">屏蔽该用户?</string>
@ -54,7 +53,6 @@
<string name="page_profile_editor">编辑个人资料</string>
<string name="delete_list">删除列表</string>
<string name="confirm_delete_list">删除列表?</string>
<string name="confirm_delete_message">删除消息?</string>
<string name="enter_username">输入用户名</string>
<string name="profile_banner">主页背景</string>
<string name="editprofile_add_banner">添加主页背景</string>

View File

@ -277,6 +277,8 @@
<dimen name="dialog_status_layout_padding">10dp</dimen>
<dimen name="dialog_status_layout_margins">5dp</dimen>
<dimen name="dialog_status_dropdown_height">28sp</dimen>
<dimen name="dialog_status_button_height">28sp</dimen>
<dimen name="dialog_status_button_padding">5dp</dimen>
<!--dimens of dialog_audio_player.xml-->
<dimen name="dialog_audio_player_height">200sp</dimen>
@ -284,6 +286,11 @@
<dimen name="dialog_audio_player_layout_padding">5dp</dimen>
<dimen name="dialog_audio_player_layout_margin">5dp</dimen>
<!--dimens of dialog_timepicker.xml-->
<dimen name="dialog_timepicker_layout_margins">5dp</dimen>
<dimen name="dialog_timepicker_button_height">28sp</dimen>
<dimen name="dialog_timepicker_button_padding">5dp</dimen>
<!--dimens of item_emoji.xml-->
<dimen name="item_emoji_layout_margin_left_right">10dp</dimen>
<dimen name="item_emoji_layout_padding">5dp</dimen>

View File

@ -217,7 +217,6 @@
<string name="status_sent_from">"sent from: "</string>
<string name="username">Username</string>
<string name="dialog_description_title">Media description</string>
<string name="confirm_cancel_message">discard message?</string>
<string name="settings_logout">log out</string>
<string name="confirm_unfollow">unfollow user?</string>
<string name="confirm_block">block user?</string>
@ -263,7 +262,6 @@
<string name="page_profile_editor">edit profile</string>
<string name="delete_list">delete userlist</string>
<string name="confirm_delete_list">delete userlist?</string>
<string name="confirm_delete_message">delete message?</string>
<string name="confirm_unfollow_list">unfollow list?</string>
<string name="trend_range">\u0020Posts</string>
<string name="enter_username">enter username</string>
@ -332,6 +330,7 @@
<string name="dialog_status_spoiler">mark content as spoiler</string>
<string name="dialog_status_visibility">Visibility</string>
<string name="dialog_status_language">Language</string>
<string name="dialog_status_schedule_set">schedule post</string>
<string name="dialog_push_repost">on repost</string>
<string name="dialog_push_favorite">on favorite</string>
<string name="dialog_push_mention">on mention</string>