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 @@ - - - _Publish - 1 - - - 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 @@ + + + + + + 0 + 23 + 1 + 1 + + + 0 + 59 + 1 + 1 + + + 0 + 59 + 1 + 1 + + + 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);