diff --git a/app/build.gradle b/app/build.gradle index dcb0a0f7..e8e73cda 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java index f352b7c8..2ba80167 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java @@ -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 mediaIds) throws ConnectionException; /** diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java index 8dca0705..1a3a1960 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java @@ -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 mediaIds) throws MastodonException { List 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) { diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/StatusUpdate.java b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/StatusUpdate.java index ac15cd6f..7b99181d 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/StatusUpdate.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/StatusUpdate.java @@ -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; } } diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/activities/StatusEditor.java b/app/src/main/java/org/nuclearfog/twidda/ui/activities/StatusEditor.java index 6fe21450..3cc252f4 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/activities/StatusEditor.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/activities/StatusEditor.java @@ -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); + } } } diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/ConfirmDialog.java b/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/ConfirmDialog.java index fa4e3e09..e8d1ebf1 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/ConfirmDialog.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/ConfirmDialog.java @@ -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; diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/StatusPreferenceDialog.java b/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/StatusPreferenceDialog.java index 5b23d91c..77a2b892 100644 --- a/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/StatusPreferenceDialog.java +++ b/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/StatusPreferenceDialog.java @@ -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); + } } \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/TimePickerDialog.java b/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/TimePickerDialog.java new file mode 100644 index 00000000..2571ffbb --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/ui/dialogs/TimePickerDialog.java @@ -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); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_status.xml b/app/src/main/res/layout/dialog_status.xml index 1cea5d1e..9cee19ae 100644 --- a/app/src/main/res/layout/dialog_status.xml +++ b/app/src/main/res/layout/dialog_status.xml @@ -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"> + + +