diff --git a/data/gresource.xml b/data/gresource.xml
index f4bc95705..7e19cded1 100644
--- a/data/gresource.xml
+++ b/data/gresource.xml
@@ -74,6 +74,7 @@
icons/scalable/actions/tuba-birthday-symbolic.svg
icons/scalable/actions/tuba-transparent-symbolic.svg
icons/scalable/actions/tuba-police-badge2-symbolic.svg
+ icons/scalable/actions/tuba-clock-alt-symbolic.svg
gtk/help-overlay.ui
@@ -110,6 +111,7 @@
ui/dialogs/preferences.ui
ui/dialogs/profile_edit.ui
ui/dialogs/filter_edit.ui
+ ui/dialogs/schedule.ui
ui/menus.ui
diff --git a/data/icons/scalable/actions/tuba-clock-alt-symbolic.svg b/data/icons/scalable/actions/tuba-clock-alt-symbolic.svg
new file mode 100644
index 000000000..b4b5066ba
--- /dev/null
+++ b/data/icons/scalable/actions/tuba-clock-alt-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/data/ui/dialogs/compose.ui b/data/ui/dialogs/compose.ui
index a4f8a126b..72648590f 100644
--- a/data/ui/dialogs/compose.ui
+++ b/data/ui/dialogs/compose.ui
@@ -36,13 +36,6 @@
-
-
-
diff --git a/data/ui/dialogs/schedule.ui b/data/ui/dialogs/schedule.ui
new file mode 100644
index 000000000..76d8878a7
--- /dev/null
+++ b/data/ui/dialogs/schedule.ui
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+ 500
+ 600
+ 360
+ 200
+
+ Schedule Date and Time Picker
+
+
+
+
+
+
diff --git a/src/API/Entity.vala b/src/API/Entity.vala
index 01a759e44..b24671806 100644
--- a/src/API/Entity.vala
+++ b/src/API/Entity.vala
@@ -16,6 +16,8 @@ public class Tuba.Entity : GLib.Object, Widgetizable, Json.Serializable {
return get_class ().find_property ("kind");
case "value":
return get_class ().find_property ("val");
+ case "params":
+ return get_class ().find_property ("props");
default:
return get_class ().find_property (name);
}
diff --git a/src/API/ScheduledStatus.vala b/src/API/ScheduledStatus.vala
new file mode 100644
index 000000000..c0d15af40
--- /dev/null
+++ b/src/API/ScheduledStatus.vala
@@ -0,0 +1,58 @@
+public class Tuba.API.ScheduledStatus : Entity, Widgetizable {
+ // NOTE: Don't forget to update in the year 3000
+ public const int DRAFT_YEAR = 2000 + 3000;
+
+ public class Params : Entity {
+ public class Poll : Entity {
+ public Gee.ArrayList options { get; set; default=new Gee.ArrayList (); }
+ public int64 expires_in { get; set; default=0; }
+ public bool multiple { get; set; default=false; }
+ public bool hide_totals { get; set; default=false; }
+
+ public override Type deserialize_array_type (string prop) {
+ switch (prop) {
+ case "options":
+ return Type.STRING;
+ }
+
+ return base.deserialize_array_type (prop);
+ }
+ }
+
+ public string text { get; set; }
+ public Poll? poll { get; set; }
+ public Gee.ArrayList? media_ids { get; set; }
+ public bool sensitive { get; set; default=false; }
+ public string? spoiler_text { get; set; }
+ public string visibility { get; set; }
+ public string? language { get; set; }
+ public string? in_reply_to_id { get; set; }
+
+ public override Type deserialize_array_type (string prop) {
+ switch (prop) {
+ case "media-ids":
+ return Type.STRING;
+ }
+
+ return base.deserialize_array_type (prop);
+ }
+ }
+
+ public string id { get; set; }
+ public string scheduled_at { get; set; }
+ public Gee.ArrayList? media_attachments { get; set; default = null; }
+ public Params? props { get; set; }
+
+ public override Type deserialize_array_type (string prop) {
+ switch (prop) {
+ case "media-attachments":
+ return typeof (API.Attachment);
+ }
+
+ return base.deserialize_array_type (prop);
+ }
+
+ public override Gtk.Widget to_widget () {
+ return new Widgets.ScheduledStatus (this);
+ }
+}
diff --git a/src/API/meson.build b/src/API/meson.build
index f34d27fb9..2579ce5f1 100644
--- a/src/API/meson.build
+++ b/src/API/meson.build
@@ -21,6 +21,7 @@ sources += files(
'Poll.vala',
'PollOption.vala',
'Relationship.vala',
+ 'ScheduledStatus.vala',
'SearchResult.vala',
'SearchResults.vala',
'Status.vala',
diff --git a/src/Application.vala b/src/Application.vala
index 9dd93790a..9ba5eef88 100644
--- a/src/Application.vala
+++ b/src/Application.vala
@@ -89,6 +89,7 @@ namespace Tuba {
{ "open-announcements", open_announcements },
{ "open-follow-requests", open_follow_requests },
{ "open-mutes-blocks", open_mutes_blocks },
+ { "open-scheduled-posts", open_scheduled_posts },
{ "open-admin-dashboard", open_admin_dashboard }
};
@@ -530,6 +531,11 @@ namespace Tuba {
close_sidebar ();
}
+ public void open_scheduled_posts () {
+ main_window.open_view (new Views.ScheduledStatuses ());
+ close_sidebar ();
+ }
+
public void open_admin_dashboard () {
new Dialogs.Admin.Window ().present ();
}
diff --git a/src/Dialogs/Composer/Dialog.vala b/src/Dialogs/Composer/Dialog.vala
index 763f7493f..3c11c3529 100644
--- a/src/Dialogs/Composer/Dialog.vala
+++ b/src/Dialogs/Composer/Dialog.vala
@@ -143,8 +143,43 @@ public class Tuba.Dialogs.Compose : Adw.Dialog {
public delegate void SuccessCallback (API.Status cb_status);
protected SuccessCallback? cb;
+ Gtk.Widget commit_button;
+ private bool _commit_button_has_menu = false;
+ public bool commit_button_has_menu {
+ get { return _commit_button_has_menu; }
+ construct {
+ _commit_button_has_menu = value;
+
+ if (value) {
+ var menu_model = new GLib.Menu ();
+ menu_model.append (_("Schedule Post"), "composer.schedule");
+
+ commit_button = new Adw.SplitButton () {
+ label = _("_Publish"),
+ use_underline = true,
+ menu_model = menu_model
+ };
+ ((Adw.SplitButton) commit_button).clicked.connect (on_commit);
+ } else {
+ commit_button = new Gtk.Button () {
+ label = _("_Publish"),
+ use_underline = true
+ };
+ ((Gtk.Button) commit_button).clicked.connect (on_commit);
+ }
+
+ header.pack_end (commit_button);
+ }
+ }
+
public string button_label {
- set { commit_button.label = value; }
+ set {
+ if (_commit_button_has_menu) {
+ ((Adw.SplitButton) commit_button).label = value;
+ } else {
+ ((Gtk.Button) commit_button).label = value;
+ }
+ }
}
public string button_class {
@@ -159,8 +194,12 @@ public class Tuba.Dialogs.Compose : Adw.Dialog {
var paste_action = new SimpleAction ("paste", null);
paste_action.activate.connect (emit_paste_signal);
+ var schedule_action = new SimpleAction ("schedule", null);
+ schedule_action.activate.connect (on_schedule_action_activated);
+
var action_group = new GLib.SimpleActionGroup ();
action_group.add_action (paste_action);
+ action_group.add_action (schedule_action);
this.insert_action_group ("composer", action_group);
add_binding_action (Gdk.Key.V, Gdk.ModifierType.CONTROL_MASK, "composer.paste", null);
@@ -269,16 +308,15 @@ public class Tuba.Dialogs.Compose : Adw.Dialog {
}
[GtkChild] unowned Adw.ViewSwitcher title_switcher;
- [GtkChild] unowned Gtk.Button commit_button;
-
[GtkChild] unowned Adw.ViewStack stack;
+ [GtkChild] unowned Adw.HeaderBar header;
public string? quote_id { get; set; }
public Compose (API.Status template = new API.Status.empty (), bool t_force_cursor_at_start = false, string? quote_id = null) {
Object (
+ commit_button_has_menu: true,
status: new BasicStatus.from_status (template),
original_status: new BasicStatus.from_status (template),
- button_label: _("_Publish"),
button_class: "suggested-action",
force_cursor_at_start: t_force_cursor_at_start,
quote_id: quote_id
@@ -384,7 +422,7 @@ public class Tuba.Dialogs.Compose : Adw.Dialog {
}
}
- [GtkCallback] void on_commit () {
+ void on_commit () {
this.sensitive = false;
transaction.begin ((obj, res) => {
try {
@@ -432,6 +470,7 @@ public class Tuba.Dialogs.Compose : Adw.Dialog {
builder.end_array ();
}
+ private string? schedule_iso8601 = null;
private Json.Builder populate_json_body () {
var builder = new Json.Builder ();
builder.begin_object ();
@@ -442,6 +481,10 @@ public class Tuba.Dialogs.Compose : Adw.Dialog {
builder.set_member_name ("quote_id");
builder.add_string_value (quote_id);
}
+ if (schedule_iso8601 != null) {
+ builder.set_member_name ("scheduled_at");
+ builder.add_string_value (schedule_iso8601);
+ }
builder.end_object ();
return builder;
@@ -472,4 +515,16 @@ public class Tuba.Dialogs.Compose : Adw.Dialog {
on_close ();
}
+ private void on_schedule_action_activated () {
+ if (!commit_button.sensitive) return;
+
+ var schedule_dlg = new Dialogs.Schedule ();
+ schedule_dlg.schedule_picked.connect (on_schedule_picked);
+ schedule_dlg.present (this);
+ }
+
+ private void on_schedule_picked (string iso8601) {
+ schedule_iso8601 = iso8601;
+ on_commit ();
+ }
}
diff --git a/src/Dialogs/Composer/Schedule.vala b/src/Dialogs/Composer/Schedule.vala
new file mode 100644
index 000000000..f21cb98fa
--- /dev/null
+++ b/src/Dialogs/Composer/Schedule.vala
@@ -0,0 +1,82 @@
+[GtkTemplate (ui = "/dev/geopjr/Tuba/ui/dialogs/schedule.ui")]
+public class Tuba.Dialogs.Schedule : Adw.Dialog {
+ public signal void schedule_picked (string iso8601);
+
+ [GtkChild] unowned Gtk.Calendar calendar;
+ [GtkChild] unowned Adw.SpinRow hours_spin_row;
+ [GtkChild] unowned Adw.SpinRow minutes_spin_row;
+ [GtkChild] unowned Adw.SpinRow seconds_spin_row;
+ [GtkChild] unowned Adw.ComboRow timezone_combo_row;
+ [GtkChild] unowned Gtk.Button schedule_button;
+
+ GLib.DateTime result_dt;
+ construct {
+ calendar.remove_css_class ("view");
+
+ string local = (new TimeZone.local ()).get_identifier ();
+ string[] timezones = { local };
+ if (local != "UTC") timezones += "UTC";
+ timezone_combo_row.model = new Gtk.StringList (timezones);
+ }
+
+ public Schedule (string? iso8601 = null, string? button_label = null) {
+ if (iso8601 == null) {
+ GLib.DateTime now = new GLib.DateTime.now_local ();
+ hours_spin_row.value = (double) now.get_hour ();
+ minutes_spin_row.value = (double) now.get_minute ();
+ seconds_spin_row.value = (double) now.get_second ();
+ } else {
+ GLib.DateTime iso8601_datetime = new GLib.DateTime.from_iso8601 (iso8601, null).to_timezone (new TimeZone.local ());
+ hours_spin_row.value = (double) iso8601_datetime.get_hour ();
+ minutes_spin_row.value = (double) iso8601_datetime.get_minute ();
+ seconds_spin_row.value = (double) iso8601_datetime.get_second ();
+
+ calendar.year = iso8601_datetime.get_year ();
+ calendar.day = iso8601_datetime.get_month ();
+ calendar.day = iso8601_datetime.get_day_of_month ();
+ }
+
+ if (button_label != null) schedule_button.label = button_label;
+
+ validate ();
+ }
+
+ [GtkCallback] void on_exit () {
+ this.force_close ();
+ }
+
+ [GtkCallback] void on_schedule () {
+ schedule_picked (result_dt.format_iso8601 ());
+ on_exit ();
+ }
+
+ [GtkCallback] void validate () {
+ bool valid = true;
+ GLib.DateTime now = new GLib.DateTime.now_utc ();
+
+ if (((Gtk.StringObject) timezone_combo_row.selected_item).string == "UTC") {
+ result_dt = new GLib.DateTime.utc (
+ calendar.year,
+ calendar.month + 1,
+ calendar.day,
+ (int) hours_spin_row.value,
+ (int) minutes_spin_row.value,
+ seconds_spin_row.value
+ );
+ } else {
+ result_dt = new GLib.DateTime.local (
+ calendar.year,
+ calendar.month + 1,
+ calendar.day,
+ (int) hours_spin_row.value,
+ (int) minutes_spin_row.value,
+ seconds_spin_row.value
+ ).to_utc ();
+ }
+
+ var delta = result_dt.difference (now);
+ if (delta < TimeSpan.HOUR) valid = delta / TimeSpan.MINUTE > 5;
+
+ schedule_button.sensitive = valid;
+ }
+}
diff --git a/src/Dialogs/Composer/meson.build b/src/Dialogs/Composer/meson.build
index bab710a6f..159e05a19 100644
--- a/src/Dialogs/Composer/meson.build
+++ b/src/Dialogs/Composer/meson.build
@@ -5,6 +5,7 @@ sources += files(
'EditorPage.vala',
'Page.vala',
'PollPage.vala',
+ 'Schedule.vala',
)
subdir('Completion')
diff --git a/src/Utils/DateTime.vala b/src/Utils/DateTime.vala
index 5df60c8cd..aa315a01a 100644
--- a/src/Utils/DateTime.vala
+++ b/src/Utils/DateTime.vala
@@ -39,8 +39,8 @@ public class Tuba.DateTime {
// %-e is the Day number
// %Y is the year (with century)
// %H is the hours (24h format)
- // %m is the minutes
- return date.to_timezone (new TimeZone.local ()).format (_("expires on %b %-e, %Y %H:%m"));
+ // %M is the minutes
+ return date.to_timezone (new TimeZone.local ()).format (_("expires on %b %-e, %Y %H:%M"));
else if (delta <= TimeSpan.MINUTE)
return _("expired on just now");
else if (delta < TimeSpan.HOUR) {
@@ -78,8 +78,8 @@ public class Tuba.DateTime {
// %-e is the Day number
// %Y is the year (with century)
// %H is the hours (24h format)
- // %m is the minutes
- return date.to_timezone (new TimeZone.local ()).format (_("%b %-e, %Y %H:%m"));
+ // %M is the minutes
+ return date.to_timezone (new TimeZone.local ()).format (_("%b %-e, %Y %H:%M"));
else if (delta <= TimeSpan.MINUTE)
return _("Just now");
else if (delta < TimeSpan.HOUR) {
@@ -117,8 +117,8 @@ public class Tuba.DateTime {
// %-e is the Day number
// %Y is the year (with century)
// %H is the hours (24h format)
- // %m is the minutes
- return date.to_timezone (new TimeZone.local ()).format (_("%b %-e, %Y %H:%m"));
+ // %M is the minutes
+ return date.to_timezone (new TimeZone.local ()).format (_("%b %-e, %Y %H:%M"));
else if (delta <= TimeSpan.MINUTE)
return _("just now");
else if (delta < TimeSpan.HOUR) {
diff --git a/src/Views/ScheduledStatuses.vala b/src/Views/ScheduledStatuses.vala
new file mode 100644
index 000000000..a9d67c03d
--- /dev/null
+++ b/src/Views/ScheduledStatuses.vala
@@ -0,0 +1,33 @@
+public class Tuba.Views.ScheduledStatuses : Views.Timeline {
+ construct {
+ url = "/api/v1/scheduled_statuses";
+ label = _("Scheduled Posts");
+ icon = "tuba-bookmarks-symbolic"; // TODO?
+ empty_state_title = _("No Scheduled Posts");
+ accepts = typeof (API.ScheduledStatus);
+ }
+
+ public override Gtk.Widget on_create_model_widget (Object obj) {
+ var widget = base.on_create_model_widget (obj);
+ var widget_scheduled = widget as Widgets.ScheduledStatus;
+
+ if (widget_scheduled != null) widget_scheduled.deleted.connect (on_deleted_scheduled);
+
+ return widget;
+ }
+
+ private void on_deleted_scheduled (string scheduled_status_id) {
+ for (uint i = 0; i < model.get_n_items (); i++) {
+ var status_obj = (API.ScheduledStatus) model.get_item (i);
+ if (status_obj.id == scheduled_status_id) {
+ model.remove (i);
+ break;
+ }
+ }
+ }
+
+ public override bool should_hide (Entity entity) {
+ var scheduled_entity = entity as API.ScheduledStatus;
+ return scheduled_entity != null && new GLib.DateTime.from_iso8601 (scheduled_entity.scheduled_at, null).get_year () > API.ScheduledStatus.DRAFT_YEAR;
+ }
+}
diff --git a/src/Views/Sidebar.vala b/src/Views/Sidebar.vala
index b96b3e348..cee3fb183 100644
--- a/src/Views/Sidebar.vala
+++ b/src/Views/Sidebar.vala
@@ -62,6 +62,7 @@ public class Tuba.Views.Sidebar : Gtk.Widget, AccountHolder {
misc_submenu_model.append (_("Announcements"), "app.open-announcements");
misc_submenu_model.append (_("Follow Requests"), "app.open-follow-requests");
misc_submenu_model.append (_("Mutes & Blocks"), "app.open-mutes-blocks");
+ misc_submenu_model.append (_("Scheduled Posts"), "app.open-scheduled-posts");
var admin_dahsboard_menu_item = new MenuItem (_("Admin Dashboard"), "app.open-admin-dashboard");
admin_dahsboard_menu_item.set_attribute_value ("hidden-when", "action-disabled");
diff --git a/src/Views/meson.build b/src/Views/meson.build
index 81d12a65c..00b7393d4 100644
--- a/src/Views/meson.build
+++ b/src/Views/meson.build
@@ -24,6 +24,7 @@ sources += files(
'NotificationRequestsList.vala',
'Notifications.vala',
'Profile.vala',
+ 'ScheduledStatuses.vala',
'Search.vala',
'Sidebar.vala',
'StatusStats.vala',
diff --git a/src/Widgets/ScheduledStatus.vala b/src/Widgets/ScheduledStatus.vala
new file mode 100644
index 000000000..0c43d4efc
--- /dev/null
+++ b/src/Widgets/ScheduledStatus.vala
@@ -0,0 +1,167 @@
+public class Tuba.Widgets.ScheduledStatus : Gtk.ListBoxRow {
+ public signal void deleted (string scheduled_status_id);
+
+ Gtk.Box content_box;
+ Gtk.Label schedule_label;
+ construct {
+ this.focusable = true;
+ this.activatable = false;
+ this.css_classes = { "card-spacing", "card" };
+ this.overflow = Gtk.Overflow.HIDDEN;
+
+ content_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
+ var action_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12) {
+ margin_top = margin_bottom = margin_start = margin_end = 6
+ };
+ schedule_label = new Gtk.Label ("") {
+ wrap = true,
+ wrap_mode = Pango.WrapMode.WORD_CHAR,
+ use_markup = true,
+ xalign = 0.0f,
+ hexpand = true,
+ margin_start = 6
+ };
+ action_box.append (schedule_label);
+
+ var actions_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
+ Gtk.Button reschedule_button = new Gtk.Button.from_icon_name ("tuba-clock-alt-symbolic") {
+ tooltip_text = _("Reschedule"),
+ css_classes = { "flat" }
+ };
+ reschedule_button.clicked.connect (on_reschedule);
+ actions_box.append (reschedule_button);
+
+ Gtk.Button delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic") {
+ css_classes = { "flat", "error" },
+ tooltip_text = _("Delete"),
+ valign = Gtk.Align.CENTER
+ };
+ delete_button.clicked.connect (on_delete);
+ actions_box.append (delete_button);
+ action_box.append (actions_box);
+
+ content_box.append (action_box);
+ this.child = content_box;
+ }
+
+ public ScheduledStatus (API.ScheduledStatus scheduled_status) {
+ Object ();
+ bind (scheduled_status);
+ }
+
+ string scheduled_at;
+ string scheduled_id;
+ Gtk.Widget? status_widget = null;
+ public void bind (API.ScheduledStatus scheduled_status) {
+ if (status_widget != null) content_box.remove (status_widget);
+
+ scheduled_at = scheduled_status.scheduled_at;
+ scheduled_id = scheduled_status.id;
+
+ API.Poll? poll = null;
+ if (scheduled_status.props.poll != null) {
+ poll = new API.Poll ("0") {
+ multiple = scheduled_status.props.poll.multiple,
+ options = new Gee.ArrayList ()
+ };
+
+ foreach (string poll_option in scheduled_status.props.poll.options) {
+ poll.options.add (new API.PollOption () {
+ title = poll_option,
+ votes_count = 0
+ });
+ }
+
+ poll.expires_at = new GLib.DateTime.now_local ().add_seconds (scheduled_status.props.poll.expires_in).format_iso8601 ();
+ }
+
+ var status = new API.Status.empty () {
+ id = scheduled_status.id,
+ account = accounts.active,
+ spoiler_text = scheduled_status.props.spoiler_text,
+ content = scheduled_status.props.text,
+ sensitive = scheduled_status.props.sensitive,
+ visibility = scheduled_status.props.visibility,
+ media_attachments = scheduled_status.media_attachments,
+ tuba_spoiler_revealed = true,
+ poll = poll,
+ created_at = scheduled_status.scheduled_at
+ };
+
+ if (scheduled_status.props.language != null) status.language = scheduled_status.props.language;
+
+ var widg = new Widgets.Status (status);
+ widg.can_be_opened = false;
+ widg.activatable = false;
+ widg.actions.visible = false;
+ widg.menu_button.visible = false;
+ widg.date_label.visible = false;
+ if (widg.poll != null) {
+ widg.poll.usable = false;
+ widg.poll.info_label.label = DateTime.humanize_ago (poll.expires_at);
+ }
+
+ // Re-parse the date into a MONTH DAY, YEAR (separator) HOUR:MINUTES
+ var date_parsed = new GLib.DateTime.from_iso8601 (scheduled_status.scheduled_at, null);
+ date_parsed = date_parsed.to_timezone (new TimeZone.local ());
+ var date_local = _("%B %e, %Y");
+ // translators: Scheduled Post title, 'scheduled for: '
+ schedule_label.label = "%s %s".printf (
+ _("Scheduled For:"),
+ date_parsed.format (@"$date_local · %H:%M").replace (" ", "") // %e prefixes with whitespace on single digits
+ );
+
+ content_box.append (widg);
+ status_widget = widg;
+ }
+
+ private void on_reschedule () {
+ var schedule_dlg = new Dialogs.Schedule (scheduled_at, _("Reschedule"));
+ schedule_dlg.schedule_picked.connect (on_schedule_picked);
+ schedule_dlg.present (this);
+ }
+
+ private void on_schedule_picked (string iso8601) {
+ new Request.PUT (@"/api/v1/scheduled_statuses/$scheduled_id")
+ .with_account (accounts.active)
+ .with_form_data ("scheduled_at", iso8601)
+ .then ((in_stream) => {
+ var parser = Network.get_parser_from_inputstream (in_stream);
+ var node = network.parse_node (parser);
+ var e = Tuba.Helper.Entity.from_json (node, typeof (API.ScheduledStatus), true);
+ if (e is API.ScheduledStatus) bind ((API.ScheduledStatus) e);
+ })
+ .on_error ((code, message) => {
+ warning (@"Error while rescheduling: $code $message");
+
+ // translators: the variable is an error
+ app.toast (_("Couldn't reschedule: %s").printf (message), 0);
+ })
+ .exec ();
+ }
+
+ private void on_delete () {
+ app.question.begin (
+ {_("Delete Scheduled Post?"), false},
+ null,
+ app.main_window,
+ { { _("Delete"), Adw.ResponseAppearance.DESTRUCTIVE }, { _("Cancel"), Adw.ResponseAppearance.DEFAULT } },
+ null,
+ false,
+ (obj, res) => {
+ if (app.question.end (res).truthy ()) {
+ new Request.DELETE (@"/api/v1/scheduled_statuses/$scheduled_id")
+ .with_account (accounts.active)
+ .then (() => {
+ deleted (scheduled_id);
+ })
+ .on_error ((code, message) => {
+ warning (@"Error while deleting scheduled status: $code $message");
+ app.toast (message, 0);
+ })
+ .exec ();
+ }
+ }
+ );
+ }
+}
diff --git a/src/Widgets/Status.vala b/src/Widgets/Status.vala
index 1103b9d95..25b50100e 100644
--- a/src/Widgets/Status.vala
+++ b/src/Widgets/Status.vala
@@ -113,7 +113,7 @@
[GtkChild] protected unowned Widgets.RichLabel name_label;
[GtkChild] protected unowned Gtk.Label handle_label;
[GtkChild] public unowned Gtk.Box indicators;
- [GtkChild] protected unowned Gtk.Label date_label;
+ [GtkChild] public unowned Gtk.Label date_label;
[GtkChild] protected unowned Gtk.Image pin_indicator;
[GtkChild] protected unowned Gtk.Image edited_indicator;
[GtkChild] protected unowned Gtk.Image visibility_indicator;
@@ -544,7 +544,7 @@
public string spoiler_text_revealed { get; set; default = _("Sensitive"); }
// separator between the bottom bar items
- string expanded_separator = "·";
+ const string EXPANDED_SEPARATOR = "·";
protected string date {
owned get {
if (expanded) {
@@ -560,7 +560,7 @@
var date_parsed = new GLib.DateTime.from_iso8601 (this.full_date, null);
date_parsed = date_parsed.to_timezone (new TimeZone.local ());
- return date_parsed.format (@"$date_local $expanded_separator %H:%M").replace (" ", ""); // %e prefixes with whitespace on single digits
+ return date_parsed.format (@"$date_local $EXPANDED_SEPARATOR %H:%M").replace (" ", ""); // %e prefixes with whitespace on single digits
} else {
return DateTime.humanize (this.full_date);
}
@@ -899,7 +899,7 @@
protected Widgets.PreviewCard prev_card;
private Widgets.Attachment.Box attachments;
private Gtk.Label translation_label;
- private Widgets.VoteBox poll;
+ public Widgets.VoteBox poll;
const string[] ALLOWED_CARD_TYPES = { "link", "video" };
ulong[] formal_handler_ids = {};
ulong[] this_handler_ids = {};
@@ -1205,7 +1205,7 @@
}
// Adds *separator* between all *flowbox* children
- private void add_separators_to_expanded_bottom (Gtk.FlowBox flowbox, string separator = expanded_separator) {
+ private void add_separators_to_expanded_bottom (Gtk.FlowBox flowbox, string separator = EXPANDED_SEPARATOR) {
var i = 0;
var child = flowbox.get_child_at_index (i);
while (child != null) {
diff --git a/src/Widgets/VoteBox.vala b/src/Widgets/VoteBox.vala
index 023f94d39..67d819455 100644
--- a/src/Widgets/VoteBox.vala
+++ b/src/Widgets/VoteBox.vala
@@ -4,7 +4,15 @@ public class Tuba.Widgets.VoteBox : Gtk.Box {
[GtkChild] protected unowned Gtk.Button button_vote;
[GtkChild] protected unowned Gtk.Button button_refresh;
[GtkChild] protected unowned Gtk.Button button_results;
- [GtkChild] protected unowned Gtk.Label info_label;
+ [GtkChild] public unowned Gtk.Label info_label;
+
+ public bool usable {
+ set {
+ button_vote.visible =
+ button_refresh.visible =
+ button_results.visible = value;
+ }
+ }
public API.Poll? poll { get; set;}
protected Gee.ArrayList selected_index = new Gee.ArrayList ();
diff --git a/src/Widgets/meson.build b/src/Widgets/meson.build
index 2501219a8..767aebc08 100644
--- a/src/Widgets/meson.build
+++ b/src/Widgets/meson.build
@@ -26,6 +26,7 @@ sources += files(
'RelationshipButton.vala',
'RichLabel.vala',
'ScaleRevealer.vala',
+ 'ScheduledStatus.vala',
'Status.vala',
'StatusActionButton.vala',
'VoteBox.vala',
diff --git a/tests/DateTime.test.vala b/tests/DateTime.test.vala
index 5b7cf667f..6ebb3d960 100644
--- a/tests/DateTime.test.vala
+++ b/tests/DateTime.test.vala
@@ -28,8 +28,8 @@ TestDate[] get_dates () {
res += TestDate () {
iso8601 = one_day.to_string (),
left = "23h left",
- ago = one_day.format ("expires on %b %-e, %Y %H:%m"),
- human = one_day.format ("%b %-e, %Y %H:%m")
+ ago = one_day.format ("expires on %b %-e, %Y %H:%M"),
+ human = one_day.format ("%b %-e, %Y %H:%M")
};
var m_one_day = time_now.add_days (-1);
@@ -44,8 +44,8 @@ TestDate[] get_dates () {
res += TestDate () {
iso8601 = one_hour.to_string (),
left = "59m left",
- ago = one_hour.format ("expires on %b %-e, %Y %H:%m"),
- human = one_hour.format ("%b %-e, %Y %H:%m")
+ ago = one_hour.format ("expires on %b %-e, %Y %H:%M"),
+ human = one_hour.format ("%b %-e, %Y %H:%M")
};
var m_one_hour = time_now.add_hours (-1);
@@ -60,8 +60,8 @@ TestDate[] get_dates () {
res += TestDate () {
iso8601 = two_minutes.to_string (),
left = "1m left",
- ago = two_minutes.format ("expires on %b %-e, %Y %H:%m"),
- human = two_minutes.format ("%b %-e, %Y %H:%m")
+ ago = two_minutes.format ("expires on %b %-e, %Y %H:%M"),
+ human = two_minutes.format ("%b %-e, %Y %H:%M")
};
var m_two_minutes = time_now.add_minutes (-2);
@@ -76,8 +76,8 @@ TestDate[] get_dates () {
res += TestDate () {
iso8601 = twenty_seconds.to_string (),
left = "expires soon",
- ago = twenty_seconds.format ("expires on %b %-e, %Y %H:%m"),
- human = twenty_seconds.format ("%b %-e, %Y %H:%m")
+ ago = twenty_seconds.format ("expires on %b %-e, %Y %H:%M"),
+ human = twenty_seconds.format ("%b %-e, %Y %H:%M")
};
var m_twenty_seconds = time_now.add_seconds (-20);