Use GLib serialization (#180)

This commit is contained in:
Bleak Grey 2020-06-20 13:04:58 +03:00 committed by GitHub
parent 287065e98b
commit 7e97ca1c54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 362 additions and 604 deletions

View File

@ -17,7 +17,6 @@ asresources = gnome.compile_resources(
)
libhandy_dep = dependency('libhandy-1', version: '>= 0.80.0')
if not libhandy_dep.found()
libhandy = subproject(
'libhandy',
@ -39,7 +38,6 @@ executable(
'src/Desktop.vala',
'src/Drawing.vala',
'src/Html.vala',
'src/Utils.vala',
'src/Request.vala',
'src/InstanceAccount.vala',
'src/Services/Streams.vala',
@ -59,6 +57,7 @@ executable(
'src/API/NotificationType.vala',
'src/API/Attachment.vala',
'src/API/Conversation.vala',
'src/API/Entity.vala',
'src/Widgets/Widgetizable.vala',
'src/Widgets/Avatar.vala',
'src/Widgets/AccountsButton.vala',
@ -90,8 +89,8 @@ executable(
dependency('glib-2.0', version: '>=2.30.0'),
dependency('gee-0.8', version: '>=0.8.5'),
dependency('granite', version: '>=5.2.0'),
dependency('json-glib-1.0'),
dependency('libsoup-2.4'),
dependency('json-glib-1.0', version: '>=1.4.4'),
libhandy_dep,
],
install: true,

View File

@ -1,6 +1,6 @@
public class Tootle.API.Account : GLib.Object {
public class Tootle.API.Account : Entity {
public int64 id { get; set; }
public string id { get; set; }
public string username { get; set; }
public string acct { get; set; }
public string? _display_name = null;
@ -19,69 +19,12 @@ public class Tootle.API.Account : GLib.Object {
public string created_at { get; set; }
public int64 followers_count { get; set; }
public int64 following_count { get; set; }
public int64 posts_count { get; set; }
public int64 statuses_count { get; set; }
public Relationship? rs { get; set; default = null; }
public Account (Json.Object obj) {
Object (
id: int64.parse (obj.get_string_member ("id")),
username: obj.get_string_member ("username"),
acct: obj.get_string_member ("acct"),
display_name: obj.get_string_member ("display_name"),
note: obj.get_string_member ("note"),
avatar: obj.get_string_member ("avatar"),
header: obj.get_string_member ("header"),
url: obj.get_string_member ("url"),
created_at: obj.get_string_member ("created_at"),
followers_count: obj.get_int_member ("followers_count"),
following_count: obj.get_int_member ("following_count"),
posts_count: obj.get_int_member ("statuses_count")
);
if (obj.has_member ("fields")) {
obj.get_array_member ("fields").foreach_element ((array, i, node) => {
var field_obj = node.get_object ();
var field_name = field_obj.get_string_member ("name");
var field_val = field_obj.get_string_member ("value");
note += "\n";
note += field_name + ": ";
note += field_val;
});
}
}
public virtual Json.Node? serialize () {
var builder = new Json.Builder ();
builder.begin_object ();
builder.set_member_name ("id");
builder.add_string_value (id.to_string ());
builder.set_member_name ("created_at");
builder.add_string_value (created_at);
builder.set_member_name ("following_count");
builder.add_int_value (following_count);
builder.set_member_name ("followers_count");
builder.add_int_value (followers_count);
builder.set_member_name ("statuses_count");
builder.add_int_value (posts_count);
builder.set_member_name ("display_name");
builder.add_string_value (display_name);
builder.set_member_name ("username");
builder.add_string_value (username);
builder.set_member_name ("acct");
builder.add_string_value (acct);
builder.set_member_name ("note");
builder.add_string_value (note);
builder.set_member_name ("header");
builder.add_string_value (header);
builder.set_member_name ("avatar");
builder.add_string_value (avatar);
builder.set_member_name ("url");
builder.add_string_value (url);
builder.end_object ();
return builder.get_root ();
}
public static Account from (Json.Node node) throws Error {
return Entity.from_json (typeof (API.Account), node) as API.Account;
}
public bool is_self () {
return id == accounts.active.id;
@ -92,7 +35,7 @@ public class Tootle.API.Account : GLib.Object {
.with_account (accounts.active)
.with_param ("id", id.to_string ())
.then_parse_array (node => {
rs = new Relationship (node.get_object ());
rs = API.Relationship.from (node);
})
.on_error (network.on_error)
.exec ();
@ -103,8 +46,8 @@ public class Tootle.API.Account : GLib.Object {
return new Request.POST (@"/api/v1/accounts/$id/$action")
.with_account (accounts.active)
.then ((sess, msg) => {
var root = network.parse (msg);
rs = new Relationship (root);
var node = network.parse_node (msg);
rs = API.Relationship.from (node);
})
.on_error (network.on_error)
.exec ();
@ -115,8 +58,8 @@ public class Tootle.API.Account : GLib.Object {
return new Request.POST (@"/api/v1/accounts/$id/$action")
.with_account (accounts.active)
.then ((sess, msg) => {
var root = network.parse (msg);
rs = new Relationship (root);
var node = network.parse_node (msg);
rs = API.Relationship.from (node);
})
.on_error (network.on_error)
.exec ();
@ -127,8 +70,8 @@ public class Tootle.API.Account : GLib.Object {
return new Request.POST (@"/api/v1/accounts/$id/$action")
.with_account (accounts.active)
.then ((sess, msg) => {
var root = network.parse (msg);
rs = new Relationship (root);
var node = network.parse_node (msg);
rs = API.Relationship.from (node);
})
.on_error (network.on_error)
.exec ();

View File

@ -1,45 +1,13 @@
public class Tootle.API.Attachment : GLib.Object {
public class Tootle.API.Attachment : Entity {
public int64 id { get; construct set; }
public string id { get; set; }
public string kind { get; set; }
public string url { get; set; }
public string? description { get; set; default = null; }
public string? _preview_url = null;
public string? description { get; set; }
public string? _preview_url { get; set; }
public string preview_url {
set { this._preview_url = value; }
get { return (_preview_url == null || _preview_url == "") ? url : _preview_url; }
}
public Attachment (Json.Object obj) {
Object (
id: int64.parse (obj.get_string_member ("id")),
kind: obj.get_string_member ("type"),
preview_url: obj.get_string_member ("preview_url"),
url: obj.get_string_member ("url"),
description: obj.get_string_member ("description")
);
}
public Json.Node? serialize () {
var builder = new Json.Builder ();
builder.begin_object ();
builder.set_member_name ("id");
builder.add_string_value (id.to_string ());
builder.set_member_name ("type");
builder.add_string_value (kind);
builder.set_member_name ("url");
builder.add_string_value (url);
builder.set_member_name ("preview_url");
builder.add_string_value (preview_url);
if (description != null) {
builder.set_member_name ("description");
builder.add_string_value (description);
}
builder.end_object ();
return builder.get_root ();
}
set { this._preview_url = value; }
get { return (this._preview_url == null || this._preview_url == "") ? url : _preview_url; }
}
}

View File

@ -1,10 +1,6 @@
public class Tootle.API.Conversation : GLib.Object, Json.Serializable, Widgetizable {
public class Tootle.API.Conversation : Entity, Widgetizable {
public string id { get; construct set; }
public bool unread { get; set; default = false; }
public Conversation () {
GLib.Object ();
}
}

143
src/API/Entity.vala Normal file
View File

@ -0,0 +1,143 @@
using Json;
public class Tootle.Entity : GLib.Object, Widgetizable, Json.Serializable {
public static string[] ignore_props = {"formal", "handle", "short-instance", "has-spoiler"};
public new ParamSpec[] list_properties () {
ParamSpec[] specs = {};
foreach (ParamSpec spec in get_class ().list_properties ()) {
if (!(spec.name in ignore_props))
specs += spec;
}
return specs;
}
public void patch (GLib.Object with) {
var props = with.get_class ().list_properties ();
foreach (var prop in props) {
var name = prop.get_name ();
var defined = get_class ().find_property (name) != null;
var forbidden = name in ignore_props;
if (defined && !forbidden) {
var val = Value (prop.value_type);
with.get_property (name, ref val);
base.set_property (name, val);
}
}
}
public static Entity 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!");
var kind = obj.get_member ("type");
if (kind != null) {
obj.set_member ("kind", kind);
obj.remove_member ("type");
}
return Json.gobject_deserialize (type, node) as Entity;
}
public Json.Node to_json () {
return Json.gobject_serialize (this);
}
public string to_json_data () {
size_t len;
return Json.gobject_to_data (this, out len);
}
public override bool deserialize_property (string prop, out Value val, ParamSpec spec, Json.Node node) {
// debug (@"deserializing $prop of type $(val.type_name ())");
var success = default_deserialize_property (prop, out val, spec, node);
var type = spec.value_type;
if (val.type () == Type.INVALID) { // Fix for glib-json < 1.5.1
val.init (type);
spec.set_value_default (ref val);
type = spec.value_type;
}
if (type.is_a (typeof (Gee.ArrayList))) {
Type contains;
switch (prop) {
case "media-attachments":
contains = typeof (API.Attachment);
break;
case "mentions":
contains = typeof (API.Mention);
break;
default:
contains = typeof (Entity);
break;
}
return des_list (out val, node, contains);
}
else if (type.is_a (typeof (API.NotificationType)))
return des_notification_type (out val, node);
return success;
}
static bool des_notification_type (out Value val, Json.Node node) {
var str = node.get_string ();
val = API.NotificationType.from_string (str);
return true;
}
static bool des_list (out Value val, Json.Node node, Type type) {
if (!node.is_null ()) {
var arr = new Gee.ArrayList<Entity> ();
node.get_array ().foreach_element ((array, i, elem) => {
var obj = Entity.from_json (type, elem);
arr.add (obj);
});
val = arr;
}
return true;
}
public override Json.Node serialize_property (string prop, Value val, ParamSpec spec) {
var type = spec.value_type;
// debug (@"serializing $prop of type $(val.type_name ())");
if (type.is_a (typeof (Gee.ArrayList)))
return ser_list (prop, val, spec);
if (type.is_a (typeof (API.NotificationType)))
return ser_notification_type (prop, val, spec);
return default_serialize_property (prop, val, spec);
}
static Json.Node ser_notification_type (string prop, Value val, ParamSpec spec) {
var enum_val = (API.NotificationType) val;
var node = new Json.Node (NodeType.VALUE);
node.set_string (enum_val.to_string ());
return node;
}
static Json.Node ser_list (string prop, Value val, ParamSpec spec) {
var list = (Gee.ArrayList<Entity>) val;
if (list == null)
return new Json.Node (NodeType.NULL);
var arr = new Json.Array ();
list.@foreach (e => {
var enode = e.to_json ();
arr.add_element (enode);
return true;
});
var node = new Json.Node (NodeType.ARRAY);
node.set_array (arr);
return node;
}
}

View File

@ -1,20 +1,11 @@
public class Tootle.API.Mention : GLib.Object {
public class Tootle.API.Mention : Entity {
public int64 id { get; construct set; }
public string id { get; construct set; }
public string username { get; construct set; }
public string acct { get; construct set; }
public string url { get; construct set; }
public Mention (Json.Object obj) {
Object (
id: int64.parse (obj.get_string_member ("id")),
username: obj.get_string_member ("username"),
acct: obj.get_string_member ("acct"),
url: obj.get_string_member ("url")
);
}
public Mention.from_account (Account account) {
public Mention.from_account (API.Account account) {
Object (
id: account.id,
username: account.username,
@ -23,19 +14,4 @@ public class Tootle.API.Mention : GLib.Object {
);
}
public Json.Node? serialize () {
var builder = new Json.Builder ();
builder.begin_object ();
builder.set_member_name ("id");
builder.add_string_value (id.to_string ());
builder.set_member_name ("username");
builder.add_string_value (username);
builder.set_member_name ("acct");
builder.add_string_value (acct);
builder.set_member_name ("url");
builder.add_string_value (url);
builder.end_object ();
return builder.get_root ();
}
}

View File

@ -1,59 +1,15 @@
public class Tootle.API.Notification : GLib.Object, Widgetizable {
public class Tootle.API.Notification : Entity, Widgetizable {
public int64 id { get; construct set; }
public Account account { get; construct set; }
public NotificationType kind { get; set; }
public string id { get; set; }
public API.Account account { get; set; }
public API.NotificationType kind { get; set; }
public string created_at { get; set; }
public Status? status { get; set; default = null; }
public Notification (Json.Object obj) throws Oopsie {
Object (
id: int64.parse (obj.get_string_member ("id")),
kind: NotificationType.from_string (obj.get_string_member ("type")),
created_at: obj.get_string_member ("created_at"),
account: new Account (obj.get_object_member ("account"))
);
if (obj.has_member ("status"))
status = new Status (obj.get_object_member ("status"));
}
public Notification.follow_request (Json.Object obj) {
Object (
id: 0,
kind: NotificationType.FOLLOW_REQUEST,
account: new Account (obj)
);
}
public API.Status? status { get; set; default = null; }
public override Gtk.Widget to_widget () {
return new Widgets.Notification (this);
}
public Json.Node? serialize () {
var builder = new Json.Builder ();
builder.begin_object ();
builder.set_member_name ("id");
builder.add_string_value (id.to_string ());
builder.set_member_name ("type");
builder.add_string_value (kind.to_string ());
builder.set_member_name ("created_at");
builder.add_string_value (created_at);
if (status != null) {
builder.set_member_name ("status");
builder.add_value (status.serialize ());
}
if (account != null) {
builder.set_member_name ("account");
builder.add_value (account.serialize ());
}
builder.end_object ();
return builder.get_root ();
}
public Soup.Message? dismiss () {
if (kind == NotificationType.WATCHLIST) {
if (accounts.active.cached_notifications.remove (this))
@ -66,7 +22,7 @@ public class Tootle.API.Notification : GLib.Object, Widgetizable {
var req = new Request.POST ("/api/v1/notifications/dismiss")
.with_account (accounts.active)
.with_param ("id", id.to_string ())
.with_param ("id", id)
.exec ();
return req;
}

View File

@ -1,6 +1,6 @@
public class Tootle.API.Relationship : GLib.Object {
public class Tootle.API.Relationship : Entity {
public int64 id { get; construct set; }
public string id { get; set; }
public bool following { get; set; default = false; }
public bool followed_by { get; set; default = false; }
public bool muting { get; set; default = false; }
@ -9,17 +9,8 @@ public class Tootle.API.Relationship : GLib.Object {
public bool blocking { get; set; default = false; }
public bool domain_blocking { get; set; default = false; }
public Relationship (Json.Object obj) {
Object (
id: int64.parse (obj.get_string_member ("id")),
following: obj.get_boolean_member ("following"),
followed_by: obj.get_boolean_member ("followed_by"),
blocking: obj.get_boolean_member ("blocking"),
muting: obj.get_boolean_member ("muting"),
muting_notifications: obj.get_boolean_member ("muting_notifications"),
requested: obj.get_boolean_member ("requested"),
domain_blocking: obj.get_boolean_member ("domain_blocking")
);
}
public static Relationship from (Json.Node node) throws Error {
return Entity.from_json (typeof (API.Relationship), node) as API.Relationship;
}
}

View File

@ -1,11 +1,10 @@
using Gee;
public class Tootle.API.Status : GLib.Object, Widgetizable {
public class Tootle.API.Status : Entity, Widgetizable {
public int64 id { get; construct set; } //TODO: IDs are no longer guaranteed to be numbers. Replace with strings.
public API.Account account { get; construct set; }
public string id { get; set; }
public API.Account account { get; set; }
public string uri { get; set; }
public string? url { get; set; default = null; }
public string? spoiler_text { get; set; default = null; }
public string? in_reply_to_id { get; set; default = null; }
public string? in_reply_to_account_id { get; set; default = null; }
@ -15,93 +14,52 @@ public class Tootle.API.Status : GLib.Object, Widgetizable {
public int64 favourites_count { get; set; default = 0; }
public string created_at { get; set; default = "0"; }
public bool reblogged { get; set; default = false; }
public bool favorited { get; set; default = false; }
public bool favourited { get; set; default = false; }
public bool sensitive { get; set; default = false; }
public bool muted { get; set; default = false; }
public bool pinned { get; set; default = false; }
public API.Visibility visibility { get; set; default = API.Visibility.PUBLIC; }
public API.Visibility visibility { get; set; default = settings.default_post_visibility; }
public API.Status? reblog { get; set; default = null; }
public ArrayList<API.Mention>? mentions { get; set; default = null; }
public ArrayList<API.Attachment>? attachments { get; set; default = null; }
public ArrayList<API.Attachment>? media_attachments { get; set; default = null; }
public string? _url { get; set; }
public string url {
owned get { return this.get_modified_url (); }
set { this._url = value; }
}
string get_modified_url () {
if (this._url == null) {
return this.uri.replace ("/activity", "");
}
return this._url;
}
public Status formal {
get { return reblog ?? this; }
}
public bool has_spoiler {
public bool has_spoiler {
get {
return formal.spoiler_text != null || formal.sensitive;
return formal.sensitive ||
!(formal.spoiler_text == null || formal.spoiler_text == "");
}
}
public Status (Json.Object obj) {
Object (
id: int64.parse (obj.get_string_member ("id")),
account: new Account (obj.get_object_member ("account")),
uri: obj.get_string_member ("uri"),
created_at: obj.get_string_member ("created_at"),
content: Html.simplify ( obj.get_string_member ("content")),
sensitive: obj.get_boolean_member ("sensitive"),
visibility: Visibility.from_string (obj.get_string_member ("visibility")),
in_reply_to_id: obj.get_string_member ("in_reply_to_id") ?? null,
in_reply_to_account_id: obj.get_string_member ("in_reply_to_account_id") ?? null,
replies_count: obj.get_int_member ("replies_count"),
reblogs_count: obj.get_int_member ("reblogs_count"),
favourites_count: obj.get_int_member ("favourites_count")
);
if (obj.has_member ("url"))
url = obj.get_string_member ("url");
else
url = obj.get_string_member ("uri").replace ("/activity", "");
var spoiler = obj.get_string_member ("spoiler_text");
if (spoiler != "")
spoiler_text = Html.simplify (spoiler);
if (obj.has_member ("reblogged"))
reblogged = obj.get_boolean_member ("reblogged");
if (obj.has_member ("favourited"))
favorited = obj.get_boolean_member ("favourited");
if (obj.has_member ("muted"))
muted = obj.get_boolean_member ("muted");
if (obj.has_member ("pinned"))
pinned = obj.get_boolean_member ("pinned");
if (obj.has_member ("reblog") && obj.get_null_member("reblog") != true)
reblog = new Status (obj.get_object_member ("reblog"));
obj.get_array_member ("mentions").foreach_element ((array, i, node) => {
var entity = node.get_object ();
if (entity != null) {
if (mentions == null)
mentions = new ArrayList<API.Mention> ();
mentions.add (new API.Mention (entity));
}
});
obj.get_array_member ("media_attachments").foreach_element ((array, i, node) => {
var entity = node.get_object ();
if (entity != null) {
if (attachments == null)
attachments = new ArrayList<API.Attachment> ();
attachments.add (new API.Attachment (entity));
}
});
}
public static Status from (Json.Node node) throws Error {
return Entity.from_json (typeof (API.Status), node) as API.Status;
}
public Status.empty () {
Object (
id: 0,
id: "",
visibility: settings.default_post_visibility
);
}
public Status.from_account (API.Account account) {
Object (
id: 0,
id: "",
account: account,
created_at: account.created_at
);
@ -121,61 +79,6 @@ public class Tootle.API.Status : GLib.Object, Widgetizable {
return w;
}
public Json.Node? serialize () {
var builder = new Json.Builder ();
builder.begin_object ();
builder.set_member_name ("id");
builder.add_string_value (id.to_string ());
builder.set_member_name ("uri");
builder.add_string_value (uri);
builder.set_member_name ("url");
builder.add_string_value (url);
builder.set_member_name ("content");
builder.add_string_value (content);
builder.set_member_name ("created_at");
builder.add_string_value (created_at);
builder.set_member_name ("visibility");
builder.add_string_value (visibility.to_string ());
builder.set_member_name ("sensitive");
builder.add_boolean_value (sensitive);
builder.set_member_name ("sensitive");
builder.add_boolean_value (sensitive);
builder.set_member_name ("replies_count");
builder.add_int_value (replies_count);
builder.set_member_name ("favourites_count");
builder.add_int_value (favourites_count);
builder.set_member_name ("reblogs_count");
builder.add_int_value (reblogs_count);
builder.set_member_name ("account");
builder.add_value (account.serialize ());
if (spoiler_text != null) {
builder.set_member_name ("spoiler_text");
builder.add_string_value (spoiler_text);
}
if (reblog != null) {
builder.set_member_name ("reblog");
builder.add_value (reblog.serialize ());
}
if (attachments != null) {
builder.set_member_name ("media_attachments");
builder.begin_array ();
foreach (API.Attachment attachment in attachments)
builder.add_value (attachment.serialize ());
builder.end_array ();
}
if (mentions != null) {
builder.set_member_name ("mentions");
builder.begin_array ();
foreach (API.Mention mention in mentions)
builder.add_value (mention.serialize ());
builder.end_array ();
}
builder.end_object ();
return builder.get_root ();
}
public bool is_owned (){
return formal.account.id == accounts.active.id;
}
@ -201,12 +104,10 @@ public class Tootle.API.Status : GLib.Object, Widgetizable {
public void action (string action, owned Network.ErrorCallback? err = network.on_error) {
new Request.POST (@"/api/v1/statuses/$(formal.id)/$action")
.with_account (accounts.active)
.then_parse_obj (obj => {
var status = new API.Status (obj).formal;
formal.reblogged = status.reblogged;
formal.favorited = status.favorited;
formal.muted = status.muted;
formal.pinned = status.pinned;
.then ((sess, msg) => {
var node = network.parse_node (msg);
var upd = API.Status.from (node).formal;
patch (upd);
})
.on_error ((status, reason) => err (status, reason))
.exec ();

View File

@ -1,13 +1,10 @@
public class Tootle.API.Tag : GLib.Object {
public class Tootle.API.Tag : Entity {
public string name { get; construct set; }
public string url { get; construct set; }
public string name { get; set; }
public string url { get; set; }
public Tag (Json.Object obj) {
Object (
name: obj.get_string_member ("name"),
url: obj.get_string_member ("url")
);
}
public static Tag from (Json.Node node) throws Error {
return Entity.from_json (typeof (API.Tag), node) as API.Tag;
}
}

View File

@ -122,7 +122,7 @@ public class Tootle.Dialogs.Compose : Window {
visibility_button.sensitive = false;
box.sensitive = false;
if (status.id > 0) {
if (status.id != "") {
info ("Removing old status...");
status.poof (publish, on_error);
}
@ -152,8 +152,8 @@ public class Tootle.Dialogs.Compose : Window {
req.with_param ("in_reply_to_account_id", status.in_reply_to_account_id);
req.then ((sess, mess) => {
var root = network.parse (mess);
var status = new API.Status (root);
var node = network.parse_node (mess);
var status = API.Status.from (node);
info ("OK: status id is %s", status.id.to_string ());
destroy ();
})

View File

@ -6,7 +6,7 @@ public class Tootle.InstanceAccount : API.Account, IStreamListener {
public string instance { get; set; }
public string client_id { get; set; }
public string client_secret { get; set; }
public string token { get; set; }
public string access_token { get; set; }
public int64 last_seen_notification { get; set; default = 0; }
public bool has_unread_notifications { get; set; default = false; }
@ -25,27 +25,11 @@ public class Tootle.InstanceAccount : API.Account, IStreamListener {
}
}
public InstanceAccount (Json.Object obj) {
Object (
username: obj.get_string_member ("username"),
instance: obj.get_string_member ("instance"),
client_id: obj.get_string_member ("id"),
client_secret: obj.get_string_member ("secret"),
token: obj.get_string_member ("access_token"),
last_seen_notification: obj.get_int_member ("last_seen_notification"),
has_unread_notifications: obj.get_boolean_member ("has_unread_notifications")
);
var cached = obj.get_object_member ("cached_profile");
var account = new API.Account (cached);
patch (account);
var notifications = obj.get_array_member ("cached_notifications");
notifications.foreach_element ((arr, i, node) => {
var notification = new API.Notification (node.get_object ());
cached_notifications.add (notification);
});
public static InstanceAccount from (Json.Node node) throws Error {
return Entity.from_json (typeof (InstanceAccount), node) as InstanceAccount;
}
public InstanceAccount () {
on_notification.connect (show_notification);
}
~InstanceAccount () {
@ -53,25 +37,25 @@ public class Tootle.InstanceAccount : API.Account, IStreamListener {
}
public InstanceAccount.empty (string instance){
Object (id: 0, instance: instance);
Object (
id: "",
instance: instance
);
}
public InstanceAccount.from_account (API.Account account) {
Object (id: account.id);
Object (
id: account.id
);
patch (account);
}
public InstanceAccount patch (API.Account account) {
Utils.merge (this, account);
return this;
}
public bool is_current () {
return accounts.active.token == token;
return accounts.active.access_token == access_token;
}
public string get_stream_url () {
return @"$instance/api/v1/streaming/?stream=user&access_token=$token";
return @"$instance/api/v1/streaming/?stream=user&access_token=$access_token";
}
public void subscribe () {
@ -82,45 +66,6 @@ public class Tootle.InstanceAccount : API.Account, IStreamListener {
streams.unsubscribe (stream, this);
}
public override Json.Node? serialize () {
var builder = new Json.Builder ();
builder.begin_object ();
builder.set_member_name ("hash");
builder.add_string_value ("test");
builder.set_member_name ("username");
builder.add_string_value (username);
builder.set_member_name ("instance");
builder.add_string_value (instance);
builder.set_member_name ("id");
builder.add_string_value (client_id);
builder.set_member_name ("secret");
builder.add_string_value (client_secret);
builder.set_member_name ("access_token");
builder.add_string_value (token);
builder.set_member_name ("last_seen_notification");
builder.add_int_value (last_seen_notification);
builder.set_member_name ("has_unread_notifications");
builder.add_boolean_value (has_unread_notifications);
var cached_profile = base.serialize ();
builder.set_member_name ("cached_profile");
builder.add_value (cached_profile);
builder.set_member_name ("cached_notifications");
builder.begin_array ();
cached_notifications.@foreach (notification => {
var node = notification.serialize ();
if (node != null)
builder.add_value (node);
return true;
});
builder.end_array ();
builder.end_object ();
return builder.get_root ();
}
protected void show_notification (API.Notification obj) {
var title = Html.remove_tags (obj.kind.get_desc (obj.account));
var notification = new GLib.Notification (title);
@ -140,21 +85,4 @@ public class Tootle.InstanceAccount : API.Account, IStreamListener {
}
}
// protected void on_status_added (API.Status status) { //TODO: Watchlist
// if (!is_current ())
// return;
// watchlist.users.@foreach (item => {
// var acct = status.account.acct;
// if (item == acct || item == "@" + acct) {
// var obj = new API.Notification (-1);
// obj.kind = API.NotificationType.WATCHLIST;
// obj.account = status.account;
// obj.status = status;
// on_notification (obj);
// }
// return true;
// });
// }
}

View File

@ -3,7 +3,7 @@ using Gee;
public class Tootle.Request : Soup.Message {
public string url { construct set; get; }
public string url { set; get; }
private Network.SuccessCallback? cb;
private Network.ErrorCallback? error_cb;
private HashMap<string, string>? pars;
@ -81,11 +81,10 @@ public class Tootle.Request : Soup.Message {
if (needs_token) {
if (account == null) {
warning (@"No account found for: $method: $url$parameters");
warning (@"No account was specified or found for $method: $url$parameters");
return this;
}
request_headers.append ("Authorization", @"Bearer $(account.token)");
request_headers.append ("Authorization", @"Bearer $(account.access_token)");
}
if (!("://" in url)) {
@ -95,7 +94,7 @@ public class Tootle.Request : Soup.Message {
this.uri = new URI (url + "" + parameters);
url = uri.to_string (false);
info (@"$method: $url");
debug (@"$method: $url");
network.queue (this, (owned) cb, (owned) error_cb);
return this;

View File

@ -19,9 +19,9 @@ public class Tootle.Accounts : GLib.Object {
new Request.GET ("/api/v1/accounts/verify_credentials")
.with_account (acc)
.then ((sess, mess) => {
var root = network.parse (mess);
var profile = new API.Account (root);
acc.patch (profile);
var node = network.parse_node (mess);
var updated = API.Account.from (node);
acc.patch (updated);
info ("OK: Token is valid");
active = acc;
settings.current_account = id;
@ -89,7 +89,7 @@ public class Tootle.Accounts : GLib.Object {
var builder = new Json.Builder ();
builder.begin_array ();
saved.foreach ((acc) => {
var node = acc.serialize ();
var node = acc.to_json ();
builder.add_value (node);
return true;
});
@ -124,8 +124,7 @@ public class Tootle.Accounts : GLib.Object {
var array = parser.get_root ().get_array ();
array.foreach_element ((_arr, _i, node) => {
var obj = node.get_object ();
var account = new InstanceAccount (obj);
var account = InstanceAccount.from (node);
if (account != null) {
saved.add (account);
account.subscribe ();

View File

@ -1,6 +1,6 @@
public interface Tootle.IStreamListener : GLib.Object {
public signal void on_status_removed (int64 id);
public signal void on_status_removed (string id);
public signal void on_status_added (API.Status s);
public signal void on_notification (API.Notification n);

View File

@ -79,10 +79,14 @@ public class Tootle.Network : GLib.Object {
app.error (_("Network Error"), message);
}
public Json.Object parse (Soup.Message msg) throws Error {
public Json.Node parse_node (Soup.Message msg) throws Error {
var parser = new Json.Parser ();
parser.load_from_data ((string) msg.response_body.flatten ().data, -1);
return parser.get_root ().get_object ();
return parser.get_root ();
}
public Json.Object parse (Soup.Message msg) throws Error {
return parse_node (msg).get_object ();
}
}

View File

@ -111,65 +111,68 @@ public class Tootle.Streams : Object {
return s.get_type ().name ();
}
static void decode (Bytes bytes, out string event, out Json.Object root) throws Error {
static void decode (Bytes bytes, out Json.Node root, out Json.Object obj, out string event) throws Error {
var msg = (string) bytes.get_data ();
var parser = new Json.Parser ();
parser.load_from_data (msg, -1);
root = parser.get_root ().get_object ();
event = root.get_string_member ("event");
root = parser.steal_root ();
obj = root.get_object ();
event = obj.get_string_member ("event");
}
static Json.Object sanitize (Json.Object root) {
var payload = root.get_string_member ("payload");
var sanitized = Soup.URI.decode (payload);
static Json.Node payload (Json.Object obj) {
var payload = obj.get_string_member ("payload");
var data = Soup.URI.decode (payload);
var parser = new Json.Parser ();
parser.load_from_data (sanitized, -1);
return parser.get_root ().get_object ();
parser.load_from_data (data, -1);
return parser.steal_root ();
}
static void emit (Bytes bytes, Connection c) throws Error {
if (!settings.live_updates)
return;
string e;
Json.Object root;
decode (bytes, out e, out root);
Json.Node root;
Json.Object root_obj;
string ev;
decode (bytes, out root, out root_obj, out ev);
// c.subscribers.@foreach (s => {
// warning ("%s: %s for %s", c.name, e, get_subscriber_name (s));
// return false;
// });
switch (e) {
switch (ev) {
case "update":
var obj = new API.Status (sanitize (root));
var node = payload (root_obj);
var status = Entity.from_json (typeof (API.Status), node) as API.Status;
c.subscribers.@foreach (s => {
s.on_status_added (obj);
s.on_status_added (status);
return true;
});
break;
case "delete":
var id = int64.parse (root.get_string_member ("payload"));
var id = root_obj.get_string_member ("payload");
c.subscribers.@foreach (s => {
s.on_status_removed (id);
return true;
});
break;
case "notification":
var obj = new API.Notification (sanitize (root));
var node = payload (root_obj);
var notif = Entity.from_json (typeof (API.Notification), node) as API.Notification;
c.subscribers.@foreach (s => {
s.on_notification (obj);
s.on_notification (notif);
return true;
});
break;
default:
warning (@"Unknown websocket event: \"$e\". Ignoring.");
warning (@"Unknown websocket event: \"$ev\". Ignoring.");
break;
}
}
public void force_delete (int64 id) {
warning (@"Force removing status id $id");
public void force_delete (string id) {
connections.get_values ().@foreach (c => {
c.subscribers.@foreach (s => {
s.on_status_removed (id);

View File

@ -1,16 +0,0 @@
public class Tootle.Utils {
public static void merge (GLib.Object what, GLib.Object with) {
var props = with.get_class ().list_properties ();
foreach (var prop in props) {
var name = prop.get_name ();
var defined = what.get_class ().find_property (name) != null;
if (defined) {
var val = Value (prop.value_type);
with.get_property (name, ref val);
what.set_property (name, val) ;
}
}
}
}

View File

@ -10,7 +10,7 @@ public class Tootle.Views.Conversations : Views.Timeline {
}
public override string? get_stream_url () {
return @"/api/v1/streaming/?stream=direct&access_token=$(accounts.active.token)";
return @"/api/v1/streaming/?stream=direct&access_token=$(account.access_token)";
}
}

View File

@ -4,24 +4,23 @@ public class Tootle.Views.ExpandedStatus : Views.Base, IAccountListener {
public API.Status root_status { get; construct set; }
protected InstanceAccount? account = null;
protected Widgets.Status root_widget;
protected Widget root_widget;
public ExpandedStatus (API.Status status) {
Object (root_status: status, state: "content");
root_widget = append (status);
root_widget.avatar.button_press_event.connect (root_widget.on_avatar_clicked);
Object (
root_status: status,
status_message: STATUS_LOADING
);
connect_account ();
}
public override void on_account_changed (InstanceAccount? acc) {
public override void on_account_changed (InstanceAccount? acc) {
account = acc;
request ();
}
private Widgets.Status prepend (API.Status status, bool to_end = false){
var w = new Widgets.Status (status);
w.avatar.button_press_event.connect (w.on_avatar_clicked);
Widget prepend (Entity entity, bool to_end = false){
var w = entity.to_widget () as Widgets.Status;
w.revealer.reveal_child = true;
if (to_end)
@ -32,8 +31,8 @@ public class Tootle.Views.ExpandedStatus : Views.Base, IAccountListener {
check_resize ();
return w;
}
private Widgets.Status append (API.Status status) {
return prepend (status, true);
Widget append (Entity entity) {
return prepend (entity, true);
}
public void request () {
@ -44,25 +43,23 @@ public class Tootle.Views.ExpandedStatus : Views.Base, IAccountListener {
var ancestors = root.get_array_member ("ancestors");
ancestors.foreach_element ((array, i, node) => {
var object = node.get_object ();
if (object != null) {
var status = new API.Status (object);
prepend (status);
}
var status = Entity.from_json (typeof (API.Status), node);
append (status);
});
root_widget = append (root_status);
var descendants = root.get_array_member ("descendants");
descendants.foreach_element ((array, i, node) => {
var object = node.get_object ();
if (object != null) {
var status = new API.Status (object);
append (status);
}
var status = Entity.from_json (typeof (API.Status), node);
append (status);
});
on_content_changed ();
int x,y;
translate_coordinates (root_widget, 0, 0, out x, out y);
scrolled.vadjustment.value = (double)(y*-1); //TODO: Animate scrolling?
scrolled.vadjustment.value = (double)(y*-1);
//content_list.select_row (root_widget);
})
.exec ();
@ -76,9 +73,9 @@ public class Tootle.Views.ExpandedStatus : Views.Base, IAccountListener {
.then ((sess, msg) => {
var root = network.parse (msg);
var statuses = root.get_array_member ("statuses");
var object = statuses.get_element (0).get_object ();
if (object != null){
var status = new API.Status (object);
var node = statuses.get_element (0);
if (node != null){
var status = API.Status.from (node);
window.open_view (new Views.ExpandedStatus (status));
}
else

View File

@ -10,7 +10,7 @@ public class Tootle.Views.Federated : Views.Timeline {
}
public override string? get_stream_url () {
return account != null ? @"$(account.instance)/api/v1/streaming/?stream=public&access_token=$(account.token)" : null;
return account != null ? @"$(account.instance)/api/v1/streaming/?stream=public&access_token=$(account.access_token)" : null;
}
}

View File

@ -8,7 +8,7 @@ public class Tootle.Views.Hashtag : Views.Timeline {
public override string? get_stream_url () {
var tag = url.substring (4);
return account != null ? @"$(account.instance)/api/v1/streaming/?stream=hashtag&tag=$tag&access_token=$(account.token)" : null;
return account != null ? @"$(account.instance)/api/v1/streaming/?stream=hashtag&tag=$tag&access_token=$(account.access_token)" : null;
}
}

View File

@ -12,7 +12,7 @@ public class Tootle.Views.Local : Views.Federated {
}
public override string? get_stream_url () {
return account != null ? @"$(account.instance)/api/v1/streaming/?stream=public:local&access_token=$(account.token)" : null;
return account != null ? @"$(account.instance)/api/v1/streaming/?stream=public:local&access_token=$(account.access_token)" : null;
}
}

View File

@ -42,7 +42,7 @@ public class Tootle.Views.NewAccount : Views.Base {
info ("New account view was requested");
}
private bool reset () {
bool reset () {
info ("State invalidated");
instance = code = client_id = client_secret = access_token = null;
instance_entry.sensitive = true;
@ -50,11 +50,11 @@ public class Tootle.Views.NewAccount : Views.Base {
return true;
}
private void oopsie (string message) {
void oopsie (string message) {
warning (message);
}
private void on_next_clicked () {
void on_next_clicked () {
try {
step ();
}
@ -63,7 +63,7 @@ public class Tootle.Views.NewAccount : Views.Base {
}
}
private void step () throws Error {
void step () throws Error {
if (instance == null)
setup_instance ();
@ -76,7 +76,7 @@ public class Tootle.Views.NewAccount : Views.Base {
request_token ();
}
private void setup_instance () throws Error {
void setup_instance () throws Error {
info ("Checking instance URL");
var str = instance_entry.text
@ -91,7 +91,7 @@ public class Tootle.Views.NewAccount : Views.Base {
throw new Oopsie.USER (_("Instance URL is invalid"));
}
private void register_client () throws Error {
void register_client () throws Error {
info ("Registering client");
instance_entry.sensitive = false;
@ -119,7 +119,7 @@ public class Tootle.Views.NewAccount : Views.Base {
.exec ();
}
private void open_confirmation_page () {
void open_confirmation_page () {
info ("Opening permission request page");
var pars = @"scope=$scopes&response_type=code&redirect_uri=$redirect_uri&client_id=$client_id";
@ -127,7 +127,7 @@ public class Tootle.Views.NewAccount : Views.Base {
Desktop.open_uri (url);
}
private void request_token () throws Error {
void request_token () throws Error {
if (code.char_count () <= 10)
throw new Oopsie.USER (_("Please paste a valid authorization code"));
@ -142,8 +142,8 @@ public class Tootle.Views.NewAccount : Views.Base {
.then ((sess, msg) => {
var root = network.parse (msg);
access_token = root.get_string_member ("access_token");
account.token = access_token;
account.id = 0;
account.access_token = access_token;
account.id = "";
info ("OK: received access token");
request_profile ();
})
@ -151,13 +151,13 @@ public class Tootle.Views.NewAccount : Views.Base {
.exec ();
}
private void request_profile () throws Error {
void request_profile () throws Error {
info ("Testing received access token");
new Request.GET ("/api/v1/accounts/verify_credentials")
.with_account (account)
.then ((sess, msg) => {
var root = network.parse (msg);
var account = new API.Account (root);
var node = network.parse_node (msg);
var account = API.Account.from (node);
info ("OK: received user profile");
save (account);
})
@ -168,13 +168,13 @@ public class Tootle.Views.NewAccount : Views.Base {
.exec ();
}
private void save (API.Account profile) {
void save (API.Account profile) {
info ("Account validated. Saving...");
account.patch (profile);
account.instance = instance;
account.client_id = client_id;
account.client_secret = client_secret;
account.token = access_token;
account.access_token = access_token;
accounts.add (account);
destroy ();

View File

@ -17,7 +17,7 @@ public class Tootle.Views.Notifications : Views.Timeline, IAccountListener, IStr
}
public override string? get_stream_url () {
return account != null ? @"$(account.instance)/api/v1/streaming/?stream=user&access_token=$(account.token)" : null;
return account != null ? @"$(account.instance)/api/v1/streaming/?stream=user&access_token=$(account.access_token)" : null;
}
public override void on_shown () {
@ -34,25 +34,14 @@ public class Tootle.Views.Notifications : Views.Timeline, IAccountListener, IStr
var nw = w as Widgets.Notification;
var notification = nw.notification;
if (notification.id > last_id)
last_id = notification.id;
if (int64.parse (notification.id) > last_id)
last_id = int64.parse (notification.id);
needs_attention = has_unread () && !current;
if (needs_attention)
accounts.save ();
}
public override GLib.Object to_entity (Json.Node node) throws Oopsie {
if (node == null)
throw new Oopsie.PARSING ("Received null Json.Node");
var obj = node.get_object ();
if (obj == null)
throw new Oopsie.PARSING ("Received Json.Node is not a Json.Object!");
return new API.Notification (obj);
}
public override void on_account_changed (InstanceAccount? acc) {
base.on_account_changed (acc);
if (account == null) {

View File

@ -57,7 +57,7 @@ public class Tootle.Views.Profile : Views.Timeline {
relationship = builder.get_object ("relationship") as Label;
posts_label = builder.get_object ("posts_label") as Label;
profile.bind_property ("posts_count", posts_label, "label", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
profile.bind_property ("statuses_count", posts_label, "label", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
var val = (int64) src;
target.set_string (_("%s Posts").printf (@"<b>$val</b>"));
return true;
@ -151,28 +151,22 @@ public class Tootle.Views.Profile : Views.Timeline {
}
public override Request append_params (Request req) {
req.with_param ("exclude_replies", (!filter_replies.active).to_string ());
req.with_param ("only_media", filter_media.active.to_string ());
return base.append_params (req);
if (page_next == null) {
req.with_param ("exclude_replies", (!filter_replies.active).to_string ());
req.with_param ("only_media", filter_media.active.to_string ());
return base.append_params (req);
}
else
return req;
}
public override GLib.Object to_entity (Json.Node node) {
var obj = node.get_object ();
if (posts_tab.active)
return new API.Status (obj);
else {
var account = new API.Account (obj);
return new API.Status.from_account (account);
}
}
public static void open_from_id (int64 id){
var url = "%s/api/v1/accounts/%lld".printf (accounts.active.instance, id);
public static void open_from_id (string id){
var url = @"$(accounts.active.instance)/api/v1/accounts/$id";
var msg = new Soup.Message ("GET", url);
msg.priority = Soup.MessagePriority.HIGH;
network.queue (msg, (sess, mess) => {
var root = network.parse (mess);
var acc = new API.Account (root);
var node = network.parse_node (mess);
var acc = API.Account.from (node);
window.open_view (new Views.Profile (acc));
}, (status, reason) => {
network.on_error (status, reason);

View File

@ -92,8 +92,7 @@ public class Tootle.Views.Search : Views.Base {
if (accounts.get_length () > 0) {
append_header (_("Accounts"));
accounts.foreach_element ((array, i, node) => {
var obj = node.get_object ();
var acc = new API.Account (obj);
var acc = API.Account.from (node);
append_account (acc);
});
}
@ -101,8 +100,7 @@ public class Tootle.Views.Search : Views.Base {
if (statuses.get_length () > 0) {
append_header (_("Statuses"));
statuses.foreach_element ((array, i, node) => {
var obj = node.get_object ();
var status = new API.Status (obj);
var status = API.Status.from (node);
append_status (status);
});
}

View File

@ -30,17 +30,6 @@ public class Tootle.Views.Timeline : IAccountListener, IStreamListener, Views.Ba
return status.is_owned ();
}
public virtual GLib.Object to_entity (Json.Node node) throws Oopsie {
if (node == null)
throw new Oopsie.PARSING ("Received null Json.Node");
var obj = node.get_object ();
if (obj == null)
throw new Oopsie.PARSING ("Received Json.Node is not a Json.Object!");
return new API.Status (obj);
}
public void prepend (Widget? w) {
append (w, true);
}
@ -90,30 +79,34 @@ public class Tootle.Views.Timeline : IAccountListener, IStreamListener, Views.Ba
public virtual string get_req_url () {
if (page_next != null)
return page_next;
return url;
}
public virtual Request append_params (Request req) {
return req.with_param ("limit", @"$(settings.timeline_page_size)");
if (page_next == null)
return req.with_param ("limit", @"$(settings.timeline_page_size)");
else
return req;
}
public virtual bool request () {
append_params (new Request.GET (get_req_url ()))
var req = append_params (new Request.GET (get_req_url ()))
.with_account (account)
.then_parse_array ((node, msg) => {
try {
var e = to_entity (node);
var e = Entity.from_json (accepts, node);
var w = e as Widgetizable;
append (w.to_widget ());
}
catch (Error e) {
warning (@"Timeline item parse error: $(e.message)");
}
get_pages (msg.response_headers.get_one ("Link"));
})
.on_error (on_error)
.exec ();
.on_error (on_error);
req.finished.connect (() => {
get_pages (req.response_headers.get_one ("Link"));
});
req.exec ();
return GLib.Source.REMOVE;
}
@ -152,7 +145,7 @@ public class Tootle.Views.Timeline : IAccountListener, IStreamListener, Views.Ba
prepend (status.to_widget ());
}
protected virtual void remove_status (int64 id) {
protected virtual void remove_status (string id) {
if (settings.live_updates) {
content.get_children ().@foreach (w => {
var sw = w as Widgets.Status;

View File

@ -4,7 +4,7 @@ using Gdk;
public class Tootle.Widgets.Attachment.Item : EventBox {
public API.Attachment attachment { get; construct set; }
private Cache.Reference? cached;
public Item (API.Attachment obj) {
@ -13,15 +13,15 @@ public class Tootle.Widgets.Attachment.Item : EventBox {
~Item () {
cache.unload (cached);
}
construct {
get_style_context ().add_class ("attachment");
width_request = height_request = 128;
hexpand = true;
tooltip_text = attachment.description ?? _("No description is available");
button_press_event.connect (on_clicked);
show ();
on_request ();
}
@ -60,7 +60,7 @@ public class Tootle.Widgets.Attachment.Item : EventBox {
var h = get_allocated_height ();
var style = get_style_context ();
var border_radius = style.get_property (Gtk.STYLE_PROPERTY_BORDER_RADIUS, style.get_state ()).get_int ();
if (cached != null) {
if (cached.loading) {
Drawing.center (ctx, w, h, 32, 32);
@ -74,7 +74,7 @@ public class Tootle.Widgets.Attachment.Item : EventBox {
ctx.fill ();
}
}
return Gdk.EVENT_STOP;
}
@ -85,15 +85,15 @@ public class Tootle.Widgets.Attachment.Item : EventBox {
}
else if (ev.button == 3) {
var menu = new Gtk.Menu ();
var item_open = new Gtk.MenuItem.with_label (_("Open"));
item_open.activate.connect (open);
menu.add (item_open);
var item_download = new Gtk.MenuItem.with_label (_("Download"));
item_download.activate.connect (download);
menu.add (item_download);
menu.show_all ();
menu.attach_widget = this;
menu.popup_at_pointer ();

View File

@ -69,18 +69,18 @@ public class Tootle.Widgets.RichLabel : Label {
var hashtags = root.get_array_member ("hashtags");
if (accounts.get_length () > 0) {
var item = accounts.get_object_element (0);
var obj = new API.Account (item);
var node = accounts.get_element (0);
var obj = API.Account.from (node);
window.open_view (new Views.Profile (obj));
}
else if (statuses.get_length () > 0) {
var item = accounts.get_object_element (0);
var obj = new API.Status (item);
var node = accounts.get_element (0);
var obj = API.Status.from (node);
window.open_view (new Views.ExpandedStatus (obj));
}
else if (hashtags.get_length () > 0) {
var item = accounts.get_object_element (0);
var obj = new API.Tag (item);
var node = accounts.get_element (0);
var obj = API.Tag.from (node);
window.open_view (new Views.Hashtag (obj.name));
}
else {

View File

@ -90,9 +90,9 @@ public class Tootle.Widgets.Status : EventBox {
kind = API.NotificationType.REBLOG_REMOTE_USER;
}
status.formal.bind_property ("favorited", favorite_button, "active", BindingFlags.SYNC_CREATE);
status.formal.bind_property ("favourited", favorite_button, "active", BindingFlags.SYNC_CREATE);
favorite_button.clicked.connect (() => {
status.action (status.formal.favorited ? "unfavourite" : "favourite");
status.action (status.formal.favourited ? "unfavourite" : "favourite");
});
status.formal.bind_property ("reblogged", reblog_button, "active", BindingFlags.SYNC_CREATE);
@ -118,7 +118,7 @@ public class Tootle.Widgets.Status : EventBox {
reblog_button.tooltip_text = _("This post can't be boosted");
}
if (status.id <= 0) {
if (status.id == "") {
actions.destroy ();
date_label.destroy ();
content.single_line_mode = true;
@ -130,7 +130,7 @@ public class Tootle.Widgets.Status : EventBox {
button_press_event.connect (open);
}
if (!attachments.populate (status.formal.attachments) || status.id <= 0) {
if (!attachments.populate (status.formal.media_attachments) || status.id == "") {
attachments.destroy ();
}