From e0b45ee49c672ffd99dad4a54fa0edea7ca97d6c Mon Sep 17 00:00:00 2001 From: bleakgrey Date: Fri, 4 May 2018 23:57:31 +0300 Subject: [PATCH] Implement attachments --- data/Application.css | 5 ++ meson.build | 2 + src/API/Attachment.vala | 23 +++++++++ src/API/Status.vala | 48 +++++++++++------- src/CacheManager.vala | 22 +++++--- src/Utils.vala | 6 ++- src/Views/AbstractView.vala | 1 + src/Views/StatusView.vala | 6 +-- src/Widgets/AttachmentWidget.vala | 44 ++++++++++++++++ src/Widgets/RichLabel.vala | 13 ++++- src/Widgets/StatusWidget.vala | 83 ++++++++++++++++++++++--------- 11 files changed, 198 insertions(+), 55 deletions(-) create mode 100644 src/API/Attachment.vala create mode 100644 src/Widgets/AttachmentWidget.vala diff --git a/data/Application.css b/data/Application.css index 5e753a8..fed0618 100644 --- a/data/Application.css +++ b/data/Application.css @@ -27,3 +27,8 @@ .header-counters{ background:rgba(255,255,255,.4); } + +.attachment{ + background: #fff; + border-radius: 3px; +} diff --git a/meson.build b/meson.build index 5643cc5..a5321a4 100644 --- a/meson.build +++ b/meson.build @@ -34,12 +34,14 @@ executable( 'src/API/StatusVisibility.vala', 'src/API/Notification.vala', 'src/API/NotificationType.vala', + 'src/API/Attachment.vala', 'src/Widgets/HeaderBar.vala', 'src/Widgets/AlignedLabel.vala', 'src/Widgets/RichLabel.vala', 'src/Widgets/AccountsButton.vala', 'src/Widgets/StatusWidget.vala', 'src/Widgets/NotificationWidget.vala', + 'src/Widgets/AttachmentWidget.vala', 'src/Dialogs/PostDialog.vala', 'src/Views/AbstractView.vala', 'src/Views/AddAccountView.vala', diff --git a/src/API/Attachment.vala b/src/API/Attachment.vala new file mode 100644 index 0000000..fc04cd3 --- /dev/null +++ b/src/API/Attachment.vala @@ -0,0 +1,23 @@ +public class Tootle.Attachment{ + + public int64 id; + public string type; + public string url; + public string preview_url; + + public Attachment(int64 id){ + this.id = id; + } + + public static Attachment parse (Json.Object obj){ + var id = int64.parse (obj.get_string_member ("id")); + var attachment = new Attachment (id); + + attachment.type = obj.get_string_member ("type"); + attachment.preview_url = obj.get_string_member ("preview_url"); + attachment.url = obj.get_string_member ("url"); + + return attachment; + } + +} diff --git a/src/API/Status.vala b/src/API/Status.vala index b3aa3ed..0321e93 100644 --- a/src/API/Status.vala +++ b/src/API/Status.vala @@ -13,9 +13,11 @@ public class Tootle.Status{ public string created_at; public bool reblogged; public bool favorited; + public bool sensitive; public Status? reblog; public Mention[]? mentions; public Tag[]? tags; + public Attachment[]? attachments; public Status(int64 id) { this.id = id; @@ -38,24 +40,7 @@ public class Tootle.Status{ status.reblogs_count = obj.get_int_member ("reblogs_count"); status.favourites_count = obj.get_int_member ("favourites_count"); status.content = Utils.escape_html ( obj.get_string_member ("content")); - - Mention[]? _mentions = {}; - obj.get_array_member ("mentions").foreach_element ((array, i, node) => { - var object = node.get_object (); - if (object != null) - _mentions += Mention.parse (object); - }); - if (_mentions.length > 0) - status.mentions = _mentions; - - Tag[]? _tags = {}; - obj.get_array_member ("tags").foreach_element ((array, i, node) => { - var object = node.get_object (); - if (object != null) - _tags += Tag.parse (object); - }); - if (_tags.length > 0) - status.tags = _tags; + status.sensitive = obj.get_boolean_member ("sensitive"); var spoiler = obj.get_string_member ("spoiler_text"); if (spoiler != "") @@ -69,6 +54,33 @@ public class Tootle.Status{ if(obj.has_member ("reblog") && obj.get_null_member("reblog") != true) status.reblog = Status.parse (obj.get_object_member ("reblog")); + Mention[]? _mentions = {}; + obj.get_array_member ("mentions").foreach_element ((array, i, node) => { + var object = node.get_object (); + if (object != null) + _mentions += Mention.parse (object); + }); + if (_mentions.length > 0) + status.mentions = _mentions; + + Tag[]? _tags = {}; + obj.get_array_member ("tags").foreach_element ((array, i, node) => { + var object = node.get_object (); + if (object != null) + _tags += Tag.parse (object); + }); + if (_tags.length > 0) + status.tags = _tags; + + Attachment[]? _attachments = {}; + obj.get_array_member ("media_attachments").foreach_element ((array, i, node) => { + var object = node.get_object (); + if (object != null) + _attachments += Attachment.parse (object); + }); + if (_attachments.length > 0) + status.attachments = _attachments; + return status; } diff --git a/src/CacheManager.vala b/src/CacheManager.vala index c96db2b..d1d15b0 100644 --- a/src/CacheManager.vala +++ b/src/CacheManager.vala @@ -9,22 +9,30 @@ public class Tootle.CacheManager : GLib.Object{ path_images = GLib.Environment.get_user_special_dir (UserDirectory.DOWNLOAD); } + //TODO: actually cache images public CacheManager(){ Object (); } - //TODO: actually cache images public void load_avatar (string url, Granite.Widgets.Avatar avatar, int size){ var msg = new Soup.Message("GET", url); - msg.finished.connect(()=>{ - uint8[] buf = msg.response_body.data; + msg.finished.connect(() => { var loader = new PixbufLoader(); loader.set_size (size, size); - loader.write(buf); + loader.write(msg.response_body.data); loader.close(); - var pixbuf = loader.get_pixbuf (); - - avatar.pixbuf = pixbuf; + avatar.pixbuf = loader.get_pixbuf (); + }); + Tootle.network.queue(msg, (sess, mess) => {}); + } + + public void load_image (string url, Gtk.Image image){ + var msg = new Soup.Message("GET", url); + msg.finished.connect(() => { + var loader = new PixbufLoader(); + loader.write(msg.response_body.data); + loader.close(); + image.set_from_pixbuf (loader.get_pixbuf ()); }); Tootle.network.queue(msg, (sess, mess) => {}); } diff --git a/src/Utils.vala b/src/Utils.vala index a0d4ecf..2a2a1ee 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -1,6 +1,10 @@ public class Tootle.Utils{ - public static string escape_html(string content){ + public static void open_url (string url) { + Gtk.show_uri (null, url, Gdk.CURRENT_TIME); + } + + public static string escape_html (string content) { var str = content .replace("
", "\n") .replace("
", "") diff --git a/src/Views/AbstractView.vala b/src/Views/AbstractView.vala index 0393847..d62d3e9 100644 --- a/src/Views/AbstractView.vala +++ b/src/Views/AbstractView.vala @@ -9,6 +9,7 @@ public abstract class Tootle.AbstractView : Gtk.ScrolledWindow { construct { view = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + view.valign = Gtk.Align.START; hscrollbar_policy = Gtk.PolicyType.NEVER; add (view); diff --git a/src/Views/StatusView.vala b/src/Views/StatusView.vala index 38465a0..0f4c65b 100644 --- a/src/Views/StatusView.vala +++ b/src/Views/StatusView.vala @@ -15,9 +15,9 @@ public class Tootle.StatusView : Tootle.AbstractView { if (is_root) widget.highlight (); - widget.content.selectable = true; - if (widget.spoiler_content != null) - widget.spoiler_content.selectable = true; + widget.content_label.selectable = true; + if (widget.content_spoiler != null) + widget.content_spoiler.selectable = true; widget.avatar.button_press_event.connect(widget.on_avatar_clicked); view.pack_start (widget, false, false, 0); } diff --git a/src/Widgets/AttachmentWidget.vala b/src/Widgets/AttachmentWidget.vala new file mode 100644 index 0000000..8180ec4 --- /dev/null +++ b/src/Widgets/AttachmentWidget.vala @@ -0,0 +1,44 @@ +using Gtk; + +public class Tootle.AttachmentWidget : Gtk.EventBox { + + Attachment attachment; + Gtk.Grid grid; + Gtk.Label? label; + Gtk.Image? image; + + construct { + margin_top = 6; + grid = new Gtk.Grid (); + get_style_context ().add_class ("attachment"); + add (grid); + } + + public AttachmentWidget (Attachment att) { + attachment = att; + var type = attachment.type; + + switch (type){ + case "image": + image = new Gtk.Image (); + image.vexpand = true; + image.margin = 3; + image.valign = Gtk.Align.CENTER; + Tootle.cache.load_image (attachment.preview_url, image); + grid.attach (image, 0, 0); + break; + default: + label = new Gtk.Label (_("Click to open %s media").printf (type)); + label.margin = 16; + grid.attach (label, 0, 0); + break; + } + + button_press_event.connect(() => { + Tootle.Utils.open_url (attachment.url); + return true; + }); + show_all (); + } + +} diff --git a/src/Widgets/RichLabel.vala b/src/Widgets/RichLabel.vala index 8f2e648..436acb3 100644 --- a/src/Widgets/RichLabel.vala +++ b/src/Widgets/RichLabel.vala @@ -12,9 +12,18 @@ public class Tootle.RichLabel : Gtk.Label { activate_link.connect (open_link); } + public void wrap_words () { + halign = Gtk.Align.START; + single_line_mode = false; + set_line_wrap (true); + wrap_mode = Pango.WrapMode.WORD_CHAR; + justify = Gtk.Justification.LEFT; + xalign = 0; + } + public bool open_link (string url){ if (mentions != null){ - foreach (Mention mention in mentions){ + foreach (Mention mention in mentions) { if (url == mention.url){ AccountView.open_from_id (mention.id); return true; @@ -34,7 +43,7 @@ public class Tootle.RichLabel : Gtk.Label { return true; } - Gtk.show_uri (null, url, Gdk.CURRENT_TIME); + Tootle.Utils.open_url (url); return true; } diff --git a/src/Widgets/StatusWidget.vala b/src/Widgets/StatusWidget.vala index 9201544..2c50238 100644 --- a/src/Widgets/StatusWidget.vala +++ b/src/Widgets/StatusWidget.vala @@ -13,9 +13,11 @@ public class Tootle.StatusWidget : Gtk.EventBox { public Gtk.Label title_date; public Gtk.Label title_acct; public Gtk.Revealer revealer; - public Tootle.RichLabel content; - public Gtk.Label? spoiler_content; + public Tootle.RichLabel content_label; + public Tootle.RichLabel content_spoiler; Gtk.Box title_box; + Gtk.Box attachments; + Gtk.ScrolledWindow attachments_scroll; Gtk.Grid grid; Gtk.Box counters; Gtk.Label reblogs; @@ -51,20 +53,10 @@ public class Tootle.StatusWidget : Gtk.EventBox { title_date = new Gtk.Label (""); title_date.ellipsize = Pango.EllipsizeMode.END; title_box.pack_end (title_date, false, false, 0); - title_box.show_all (); - content = new RichLabel (""); - content.halign = Gtk.Align.START; - content.single_line_mode = false; - content.set_line_wrap (true); - content.wrap_mode = Pango.WrapMode.WORD_CHAR; - content.justify = Gtk.Justification.LEFT; - content.margin_end = 12; - content.xalign = 0; - revealer = new Revealer (); - revealer.reveal_child = true; - revealer.add (content); + content_label = new RichLabel (""); + content_label.wrap_words (); reblogs = new Gtk.Label ("0"); favorites = new Gtk.Label ("0"); @@ -88,6 +80,20 @@ public class Tootle.StatusWidget : Gtk.EventBox { PostDialog.open_reply (Tootle.window, this.status); }); + attachments = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + attachments.hexpand = true; + attachments_scroll = new ScrolledWindow (null, null); + attachments_scroll.vscrollbar_policy = Gtk.PolicyType.NEVER; + attachments_scroll.add (attachments); + + var revealer_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6); + revealer_box.margin_end = 12; + revealer_box.add (content_label); + revealer_box.add (attachments_scroll); + revealer = new Revealer (); + revealer.reveal_child = true; + revealer.add (revealer_box); + counters = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); //TODO: currently useless counters.margin_top = 6; counters.margin_bottom = 6; @@ -103,7 +109,7 @@ public class Tootle.StatusWidget : Gtk.EventBox { grid.attach (revealer, 2, 4, 1, 1); grid.attach (counters, 2, 5, 1, 1); add (grid); - show_all (); //TODO: display conversations + show_all (); } public StatusWidget (Status status) { @@ -128,24 +134,49 @@ public class Tootle.StatusWidget : Gtk.EventBox { grid.attach (label, 2, 0, 2, 1); } - if (status.spoiler_text != null){ + if (is_spoiler ()){ revealer.reveal_child = false; - spoiler_button = new Button.with_label (_("Toggle content")); - spoiler_content = new RichLabel (status.spoiler_text); - var spoiler_box = new Box (Gtk.Orientation.HORIZONTAL, 6); - spoiler_box.add (spoiler_content); + spoiler_box.margin_end = 12; + + var spoiler_button_text = _("Toggle content"); + if (status.sensitive && status.attachments != null) { + spoiler_button = new Button.from_icon_name ("mail-attachment-symbolic", Gtk.IconSize.BUTTON); + spoiler_button.label = spoiler_button_text; + spoiler_button.always_show_image = true; + spoiler_button.hexpand = true; + spoiler_button.halign = Gtk.Align.END; + content_label.margin_top = 6; + } + else { + spoiler_button = new Button.with_label (spoiler_button_text); + spoiler_button.hexpand = true; + spoiler_button.halign = Gtk.Align.END; + } + spoiler_button.clicked.connect (() => revealer.set_reveal_child (!revealer.child_revealed)); + + var spoiler_text = _("[ This post contains sensitive content ]"); + if (status.spoiler_text != null) + spoiler_text = status.spoiler_text; + content_spoiler = new RichLabel (spoiler_text); + content_spoiler.wrap_words (); + + spoiler_box.add (content_spoiler); spoiler_box.add (spoiler_button); spoiler_box.show_all (); - - spoiler_button.clicked.connect (() => revealer.set_reveal_child (!revealer.child_revealed)); grid.attach (spoiler_box, 2, 3, 1, 1); } + if (status.attachments != null) { + foreach (Attachment attachment in status.attachments) + attachments.add (new AttachmentWidget (attachment)); + } + destroy.connect (() => { if(separator != null) separator.destroy (); }); + rebind (); } @@ -157,8 +188,8 @@ public class Tootle.StatusWidget : Gtk.EventBox { public void rebind (){ title_user.label = "%s".printf (status.get_formal ().account.display_name); title_acct.label = "@" + status.account.acct; - content.label = status.content; - content.mentions = status.mentions; + content_label.label = status.content; + content_label.mentions = status.mentions; parse_date_iso8601 (status.created_at); reblogs.label = status.reblogs_count.to_string (); @@ -173,6 +204,10 @@ public class Tootle.StatusWidget : Gtk.EventBox { Tootle.cache.load_avatar (status.get_formal ().account.avatar, this.avatar, this.avatar_size); } + + public bool is_spoiler (){ + return status.spoiler_text != null || status.sensitive; + } // elementary OS has old GLib, so I have to improvise // Version >=2.56 provides DateTime.from_iso8601