diff --git a/data/gresource.xml b/data/gresource.xml
index cf71d3b..650e28c 100644
--- a/data/gresource.xml
+++ b/data/gresource.xml
@@ -61,6 +61,7 @@
ui/widgets/list_item.ui
ui/widgets/list_editor_item.ui
ui/widgets/compose_attachment.ui
+ ui/widgets/votebox.ui
ui/dialogs/new_account.ui
ui/dialogs/compose.ui
ui/dialogs/main.ui
diff --git a/data/ui/widgets/status.ui b/data/ui/widgets/status.ui
index 2e75a5b..bb3281f 100644
--- a/data/ui/widgets/status.ui
+++ b/data/ui/widgets/status.ui
@@ -218,6 +218,10 @@
True
+
+
+
diff --git a/data/ui/widgets/votebox.ui b/data/ui/widgets/votebox.ui
new file mode 100644
index 0000000..8f08797
--- /dev/null
+++ b/data/ui/widgets/votebox.ui
@@ -0,0 +1,37 @@
+
+
+
+
+ 12
+ vertical
+
+
+
+
+
+
+
+
diff --git a/flake.lock b/flake.lock
index 8856779..f47db58 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,4 +1,4 @@
-{
+gi{
"nodes": {
"nixpkgs": {
"locked": {
diff --git a/meson.build b/meson.build
index 2f5b461..585678d 100644
--- a/meson.build
+++ b/meson.build
@@ -57,6 +57,8 @@ sources = files(
'src/API/SearchResults.vala',
'src/API/Status.vala',
'src/API/Tag.vala',
+ 'src/API/Poll.vala',
+ 'src/API/PollOption.vala',
'src/Application.vala',
'src/Dialogs/Composer/AttachmentsPage.vala',
'src/Dialogs/Composer/Dialog.vala',
@@ -120,6 +122,8 @@ sources = files(
'src/Widgets/Status.vala',
'src/Widgets/StatusActionButton.vala',
'src/Widgets/Widgetizable.vala',
+ 'src/Widgets/VoteBox.vala',
+ 'src/Widgets/VoteCheckButton.vala',
)
build_file = configure_file(
diff --git a/po/dev.geopjr.tooth.pot b/po/dev.geopjr.tooth.pot
index f9ba621..b7d3f3e 100644
--- a/po/dev.geopjr.tooth.pot
+++ b/po/dev.geopjr.tooth.pot
@@ -295,6 +295,14 @@ msgstr ""
msgid "Delete"
msgstr ""
+#: src/Widgets/VoteBox.vala:17
+msgid "Vote"
+msgstr ""
+
+#: src/Widgets/VoteBox.vala:87
+msgid "Expires at: %s"
+msgstr ""
+
#: data/ui/widgets/list_item.ui:26 src/Dialogs/ListEditor.vala:87
msgid "Untitled"
msgstr ""
diff --git a/src/API/Entity.vala b/src/API/Entity.vala
index 9f2808b..cb6c638 100644
--- a/src/API/Entity.vala
+++ b/src/API/Entity.vala
@@ -103,7 +103,7 @@ public class Tooth.Entity : GLib.Object, Widgetizable, Json.Serializable {
return success;
}
- static bool des_list (out Value val, Json.Node node, Type type) {
+ public static bool des_list (out Value val, Json.Node node, Type type) {
if (!node.is_null ()) {
var arr = new Gee.ArrayList ();
node.get_array ().foreach_element ((array, i, elem) => {
diff --git a/src/API/Poll.vala b/src/API/Poll.vala
new file mode 100755
index 0000000..97347bf
--- /dev/null
+++ b/src/API/Poll.vala
@@ -0,0 +1,78 @@
+using Gee;
+using Json;
+
+public class Tooth.API.Poll : GLib.Object, Json.Serializable{
+ public string id { get; set; }
+ public string expires_at{ get; set; }
+ public bool expired { get; set; }
+ public bool multiple { get; set; }
+ public int64 votes_count { get; set; }
+ public int64 voters_count { get; set; }
+ public bool voted { get; set; default = true;}
+ public ArrayList own_votes { get; set; }
+ public ArrayList? options{ get; set; default = null; }
+
+ public Poll (string _id) {
+ id = _id;
+ }
+
+ public override bool deserialize_property (string prop, out Value val, ParamSpec spec, Json.Node node) {
+ var success = default_deserialize_property (prop, out val, spec, node);
+
+ var type = spec.value_type;
+ if (prop=="options"){
+ return Entity.des_list (out val, node, typeof (API.PollOption));
+ }
+ if (prop=="own-votes"){
+ return Poll.des_list_int (out val, node);
+ }
+ return success;
+ }
+ public static bool des_list_int (out Value val, Json.Node node) {
+ if (!node.is_null ()) {
+ var arr = new Gee.ArrayList ();
+ node.get_array ().foreach_element ((array, i, elem) => {
+ arr.add ((int)elem.get_int());
+ });
+ val = arr;
+ }
+ return true;
+ }
+ public static Poll from_json (Type type, Json.Node? node) throws Oopsie {
+ if (node == null)
+ throw new Oopsie.PARSING (@"Received Json.Node for $(type.name ()) is null!");
+
+ var obj = node.get_object ();
+ if (obj == null)
+ throw new Oopsie.PARSING (@"Received Json.Node for $(type.name ()) is not a Json.Object!");
+
+ return Json.gobject_deserialize (type, node) as Poll;
+ }
+ public static Request vote (InstanceAccount acc,ArrayList options,ArrayList selection, string id) {
+ message (@"Voting poll $(id)...");
+ //Creating json to send
+ var builder = new Json.Builder ();
+ builder.begin_object ();
+ builder.set_member_name ("choices");
+ builder.begin_array ();
+ var row_number=0;
+ foreach (API.PollOption p in options){
+ foreach (string select in selection){
+ if (select == p.title){
+ builder.add_string_value (row_number.to_string());
+ }
+ }
+ row_number++;
+ }
+ builder.end_array ();
+ builder.end_object ();
+ var generator = new Json.Generator ();
+ generator.set_root (builder.get_root ());
+ var json = generator.to_data (null);
+ //Send POST MESSAGE
+ Request voting=new Request.POST (@"/api/v1/polls/$(id)/votes")
+ .with_account (acc);
+ voting.set_request("application/json",Soup.MemoryUse.COPY,json.data);
+ return voting;
+ }
+}
diff --git a/src/API/PollOption.vala b/src/API/PollOption.vala
new file mode 100755
index 0000000..59799fe
--- /dev/null
+++ b/src/API/PollOption.vala
@@ -0,0 +1,4 @@
+public class Tooth.API.PollOption: Entity {
+ public string? title { get; set; }
+ public int64 votes_count{ get; set; }
+}
diff --git a/src/API/Status.vala b/src/API/Status.vala
index 897735e..fbc109d 100644
--- a/src/API/Status.vala
+++ b/src/API/Status.vala
@@ -27,6 +27,7 @@ public class Tooth.API.Status : Entity, Widgetizable {
public API.Status? reblog { get; set; default = null; }
public ArrayList? mentions { get; set; default = null; }
public ArrayList? media_attachments { get; set; default = null; }
+ public API.Poll? poll { get; set; default = null; }
public string? t_url { get; set; }
public string url {
diff --git a/src/Utils/DateTime.vala b/src/Utils/DateTime.vala
index 547cf04..e14cd87 100644
--- a/src/Utils/DateTime.vala
+++ b/src/Utils/DateTime.vala
@@ -2,12 +2,62 @@ using GLib;
public class Tooth.DateTime {
+ public static string humanize_left (string iso8601) {
+ var date = new GLib.DateTime.from_iso8601 (iso8601, null);
+ var now = new GLib.DateTime.now_local ();
+ var delta = date.difference (now);
+ if (delta < 0) {
+ return humanize(iso8601);
+ } else if (delta <= TimeSpan.MINUTE) {
+ return _("expires soon");
+ } else if (delta < TimeSpan.HOUR) {
+ var minutes = delta / TimeSpan.MINUTE;
+ return _(@"$(minutes)m left");
+ } else if (delta <= TimeSpan.DAY) {
+ var hours = delta / TimeSpan.HOUR;
+ return _(@"$(hours)h left");
+ } else if (delta <= (TimeSpan.DAY * 60)) {
+ var days = delta / TimeSpan.DAY;
+ return _(@"$(days)d left");
+ } else {
+ return date.format (_("expires on %b %e, %Y"));
+ }
+ }
+
+ public static string humanize_ago (string iso8601) {
+ var date = new GLib.DateTime.from_iso8601 (iso8601, null);
+ var now = new GLib.DateTime.now_local ();
+ var delta = now.difference (date);
+ if (delta < 0)
+ return date.format (_("expires on %b %e, %Y %H:%m"));
+ else if (delta <= TimeSpan.MINUTE)
+ return _("expired on just now");
+ else if (delta < TimeSpan.HOUR) {
+ var minutes = delta / TimeSpan.MINUTE;
+ return _(@"expired $(minutes)m ago");
+ }
+ else if (delta <= TimeSpan.DAY) {
+ var hours = delta / TimeSpan.HOUR;
+ return _(@"expired $(hours)h ago");
+ }
+ else if (is_same_day (now, date.add_days (1))) {
+ return _("expired yesterday");
+ }
+ else if (date.get_year () == now.get_year ()) {
+ return date.format (_("expired on %b %e"));
+ }
+ else {
+ return date.format (_("expired on %b %e, %Y"));
+ }
+ }
+
public static string humanize (string iso8601) {
var date = new GLib.DateTime.from_iso8601 (iso8601, null);
var now = new GLib.DateTime.now_local ();
var delta = now.difference (date);
-
- if (delta <= TimeSpan.MINUTE)
+ if (delta < 0)
+ return date.format (_("%b %e, %Y %H:%m"));
+ else if (delta <= TimeSpan.MINUTE)
return _("Just now");
else if (delta < TimeSpan.HOUR) {
var minutes = delta / TimeSpan.MINUTE;
diff --git a/src/Widgets/Status.vala b/src/Widgets/Status.vala
index d9d4195..283d981 100644
--- a/src/Widgets/Status.vala
+++ b/src/Widgets/Status.vala
@@ -58,6 +58,8 @@ public class Tooth.Widgets.Status : ListBoxRow {
[GtkChild] public unowned Box actions;
+ [GtkChild] public unowned Widgets.VoteBox poll;
+
protected Button reply_button;
protected Adw.ButtonContent reply_button_content;
protected StatusActionButton reblog_button;
@@ -247,6 +249,14 @@ public class Tooth.Widgets.Status : ListBoxRow {
date_label.destroy ();
}
+ if (status.poll==null){
+ poll.hide();
+ }
+ else{
+ poll.status_parent=status;
+ status.bind_property ("poll", poll, "poll", BindingFlags.SYNC_CREATE);
+ }
+
// Attachments
attachments.list = status.formal.media_attachments;
}
diff --git a/src/Widgets/VoteBox.vala b/src/Widgets/VoteBox.vala
new file mode 100644
index 0000000..c9f4236
--- /dev/null
+++ b/src/Widgets/VoteBox.vala
@@ -0,0 +1,139 @@
+using Gtk;
+using Gdk;
+using Gee;
+
+[GtkTemplate (ui = "/dev/geopjr/tooth/ui/widgets/votebox.ui")]
+public class Tooth.Widgets.VoteBox: Box {
+ [GtkChild] protected ListBox pollBox;
+ [GtkChild] protected Button button_vote;
+ [GtkChild] protected Box pollActionBox;
+ [GtkChild] protected Label people_label;
+ [GtkChild] protected Label expires_label;
+
+ public API.Poll? poll { get; set;}
+ public API.Status? status_parent{ get; set;}
+
+
+ protected ArrayList selectedIndex=new ArrayList();
+
+ construct{
+ button_vote.set_label (_("Vote"));
+ button_vote.clicked.connect ((button) =>{
+ Request voting=API.Poll.vote(accounts.active,poll.options,selectedIndex,poll.id);
+ voting.then ((sess, mess) => {
+ status_parent.poll=API.Poll.from_json(typeof(API.Poll),network.parse_node (mess));
+ })
+ .on_error ((code, reason) => {}).exec ();
+ });
+ notify["poll"].connect (update);
+ button_vote.sensitive = false;
+ }
+
+ public string generate_css_style(int percentage) {
+ return @".ttl-poll-$(percentage).ttl-poll-winner { background: linear-gradient(to right, alpha(@accent_bg_color, .5) $(percentage)%, transparent 0%); } .ttl-poll-$(percentage) { background: linear-gradient(to right, alpha(@view_fg_color, .1) $(percentage)%, transparent 0%); }";
+ }
+
+ void update(){
+ var row_number=0;
+ var winner_p = 0.0;
+
+ Adw.ActionRow last_winner = null;
+ Widgets.VoteCheckButton group_radio_option = null;
+
+ //clear all existing entries
+ Widget entry=pollBox.get_first_child();
+ while(entry!=null){
+ pollBox.remove(entry);
+ entry=pollBox.get_first_child();
+ }
+ //Reset button visibility
+ button_vote.set_visible(false);
+ if(!poll.expired && !poll.voted){
+ button_vote.set_visible(true);
+ }
+
+ // if (poll.expired) {
+ // pollBox.sensitive = false;
+ // }
+ //creates the entries of poll
+ foreach (API.PollOption p in poll.options){
+ var row = new Adw.ActionRow ();
+ //if it is own poll
+ if(poll.expired || poll.voted){
+ // If multiple, Checkbox else radioButton
+ var percentage = ((double)p.votes_count/poll.votes_count)*100;
+
+ var provider = new Gtk.CssProvider ();
+ provider.load_from_data(generate_css_style((int) percentage).data);
+ row.get_style_context ().add_provider (provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+ row.add_css_class(@"ttl-poll-$((int) percentage)");
+
+ if (percentage > winner_p) {
+ winner_p = percentage;
+ if (last_winner != null)
+ last_winner.remove_css_class("ttl-poll-winner");
+ row.add_css_class("ttl-poll-winner");
+ last_winner = row;
+ }
+
+ foreach (int own_vote in poll.own_votes){
+ if (own_vote==row_number){
+ row.add_suffix(new Image.from_icon_name("tooth-check-round-outline-symbolic"));
+ }
+ }
+
+ row.title = "%.1f%%".printf(percentage);
+ row.subtitle = p.title;
+ pollBox.append(row);
+ }
+ else{
+ row.title = p.title;
+ var check_option = new Widgets.VoteCheckButton ();
+
+ if (!poll.multiple){
+ if (row_number==0){
+ group_radio_option=check_option;
+ }
+ else{
+ check_option.set_group(group_radio_option);
+ }
+ }
+
+ check_option.poll_title = p.title;
+ check_option.toggled.connect((radio)=>{
+ var radio_votebutton = radio as Widgets.VoteCheckButton;
+ if (selectedIndex.contains(radio_votebutton.poll_title)){
+ selectedIndex.remove(radio_votebutton.poll_title);
+ }
+ else{
+ selectedIndex.add(radio_votebutton.poll_title);
+ }
+ button_vote.sensitive = selectedIndex.size > 0;
+ });
+
+ foreach (int own_vote in poll.own_votes){
+ if (own_vote==row_number){
+ check_option.set_active(true);
+ row.add_suffix(new Image.from_icon_name("tooth-check-round-outline-symbolic"));
+ if (!selectedIndex.contains(p.title)){
+ selectedIndex.add(p.title);
+ }
+ }
+ }
+
+ if(poll.expired || poll.voted){
+ check_option.set_sensitive(false);
+ }
+
+ row.add_prefix(check_option);
+ row.activatable_widget = check_option;
+
+ pollBox.append(row);
+ }
+ row_number++;
+ }
+
+ people_label.label = _("%lld voted").printf(poll.votes_count);
+ expires_label.label = poll.expired ? DateTime.humanize_ago(poll.expires_at) : DateTime.humanize_left(poll.expires_at);
+ }
+}
diff --git a/src/Widgets/VoteCheckButton.vala b/src/Widgets/VoteCheckButton.vala
new file mode 100644
index 0000000..7548211
--- /dev/null
+++ b/src/Widgets/VoteCheckButton.vala
@@ -0,0 +1,11 @@
+using Gtk;
+using Gdk;
+
+public class Tooth.Widgets.VoteCheckButton : CheckButton {
+ public string poll_title { get; set;}
+
+ public VoteCheckButton () {
+ Object ();
+ this.add_css_class("selection-mode");
+ }
+}