Refactor entity resolving
This commit is contained in:
parent
6860ed28c2
commit
9ca9720f60
|
@ -70,6 +70,7 @@ executable(
|
|||
'src/API/Attachment.vala',
|
||||
'src/API/Conversation.vala',
|
||||
'src/API/List.vala',
|
||||
'src/API/SearchResults.vala',
|
||||
'src/API/Entity.vala',
|
||||
'src/Widgets/Widgetizable.vala',
|
||||
'src/Widgets/Avatar.vala',
|
||||
|
|
|
@ -37,11 +37,30 @@ public class Tootle.API.Account : Entity, Widgetizable {
|
|||
return id == accounts.active.id;
|
||||
}
|
||||
|
||||
public override bool is_local (InstanceAccount account) {
|
||||
return account.short_instance in url;
|
||||
}
|
||||
|
||||
public override Gtk.Widget to_widget () {
|
||||
var status = new API.Status.from_account (this);
|
||||
return new Widgets.Status (status);
|
||||
}
|
||||
|
||||
public override void open () {
|
||||
var view = new Views.Profile (this);
|
||||
window.open_view (view);
|
||||
}
|
||||
|
||||
public override void resolve_open (InstanceAccount account) {
|
||||
if (is_local (account))
|
||||
open ();
|
||||
else {
|
||||
account.resolve.begin (url, (obj, res) => {
|
||||
account.resolve.end (res).open ();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public Request get_relationship () {
|
||||
return new Request.GET ("/api/v1/accounts/relationships")
|
||||
.with_account (accounts.active)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
public class Tootle.API.Conversation : Entity, Widgetizable {
|
||||
public class Tootle.API.Conversation : Entity, Widgetizable {
|
||||
|
||||
public string id { get; construct set; }
|
||||
public bool unread { get; set; default = false; }
|
||||
|
|
|
@ -4,6 +4,10 @@ public class Tootle.Entity : GLib.Object, Widgetizable, Json.Serializable {
|
|||
|
||||
public static string[] ignore_props = {"formal", "handle", "short-instance", "has-spoiler"};
|
||||
|
||||
public virtual bool is_local (InstanceAccount account) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public new ParamSpec[] list_properties () {
|
||||
ParamSpec[] specs = {};
|
||||
foreach (ParamSpec spec in get_class ().list_properties ()) {
|
||||
|
@ -13,7 +17,6 @@ public class Tootle.Entity : GLib.Object, Widgetizable, Json.Serializable {
|
|||
return specs;
|
||||
}
|
||||
|
||||
|
||||
public void patch (GLib.Object with) {
|
||||
var props = with.get_class ().list_properties ();
|
||||
foreach (var prop in props) {
|
||||
|
@ -86,6 +89,15 @@ public class Tootle.Entity : GLib.Object, Widgetizable, Json.Serializable {
|
|||
case "fields":
|
||||
contains = typeof (API.AccountField);
|
||||
break;
|
||||
case "accounts":
|
||||
contains = typeof (API.Account);
|
||||
break;
|
||||
case "statuses":
|
||||
contains = typeof (API.Status);
|
||||
break;
|
||||
case "hashtags":
|
||||
contains = typeof (API.Tag);
|
||||
break;
|
||||
default:
|
||||
contains = typeof (Entity);
|
||||
break;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
public class Tootle.API.Mention : Entity {
|
||||
public class Tootle.API.Mention : Entity, Widgetizable {
|
||||
|
||||
public string id { get; construct set; }
|
||||
public string username { get; construct set; }
|
||||
|
@ -14,4 +14,8 @@ public class Tootle.API.Mention : Entity {
|
|||
);
|
||||
}
|
||||
|
||||
public override void open () {
|
||||
Views.Profile.open_from_id (id);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
using Gee;
|
||||
|
||||
public class Tootle.API.SearchResults : Entity {
|
||||
|
||||
public ArrayList<API.Account> accounts { get; set; }
|
||||
public ArrayList<API.Status> statuses { get; set; }
|
||||
public ArrayList<API.Tag> hashtags { get; set; }
|
||||
|
||||
public static SearchResults from (Json.Node node) throws Error {
|
||||
return Entity.from_json (typeof (SearchResults), node) as SearchResults;
|
||||
}
|
||||
|
||||
public Entity first () throws Error {
|
||||
if (accounts.size > 0)
|
||||
return accounts[0];
|
||||
else if (statuses.size > 0)
|
||||
return statuses[0];
|
||||
else if (hashtags.size > 0)
|
||||
return hashtags[0];
|
||||
else
|
||||
throw new Oopsie.INTERNAL (_("Search returned no results"));
|
||||
}
|
||||
|
||||
public static async SearchResults request (string q, InstanceAccount account) throws Error {
|
||||
var req = new Request.GET ("/api/v2/search")
|
||||
.with_account (account)
|
||||
.with_param ("resolve", "true")
|
||||
.with_param ("q", Soup.URI.encode (q, null));
|
||||
yield req.await ();
|
||||
|
||||
return from (network.parse_node (req));
|
||||
}
|
||||
|
||||
}
|
|
@ -77,6 +77,11 @@ public class Tootle.API.Status : Entity, Widgetizable {
|
|||
return new Widgets.Status (this);
|
||||
}
|
||||
|
||||
public override void open () {
|
||||
var view = new Views.ExpandedStatus (formal);
|
||||
window.open_view (view);
|
||||
}
|
||||
|
||||
public bool is_owned (){
|
||||
return formal.account.id == accounts.active.id;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
public class Tootle.API.Tag : Entity {
|
||||
using Gtk;
|
||||
|
||||
public class Tootle.API.Tag : Entity, Widgetizable {
|
||||
|
||||
public string name { get; set; }
|
||||
public string url { get; set; }
|
||||
|
@ -7,4 +9,14 @@ public class Tootle.API.Tag : Entity {
|
|||
return Entity.from_json (typeof (API.Tag), node) as API.Tag;
|
||||
}
|
||||
|
||||
public override Widget to_widget () {
|
||||
var encoded = Soup.URI.encode (name, null);
|
||||
var w = new Widgets.RichLabel (@"<a href=\"$(accounts.active.instance)/tags/$encoded\">#$name</a>");
|
||||
w.use_markup = true;
|
||||
w.halign = Align.START;
|
||||
w.margin = 8;
|
||||
w.show ();
|
||||
return w;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,86 +3,89 @@ using Gee;
|
|||
|
||||
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 access_token { get; set; }
|
||||
public string instance { get; set; }
|
||||
public string client_id { get; set; }
|
||||
public string client_secret { 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; }
|
||||
public ArrayList<API.Notification> cached_notifications { get; set; default = new ArrayList<API.Notification> (); }
|
||||
public int64 last_seen_notification { get; set; default = 0; }
|
||||
public bool has_unread_notifications { get; set; default = false; }
|
||||
public ArrayList<API.Notification> cached_notifications { get; set; default = new ArrayList<API.Notification> (); }
|
||||
|
||||
protected string? stream;
|
||||
|
||||
public new string handle {
|
||||
owned get { return @"@$username@$short_instance"; }
|
||||
}
|
||||
public string short_instance {
|
||||
owned get {
|
||||
return instance
|
||||
.replace ("https://", "")
|
||||
.replace ("/","");
|
||||
}
|
||||
}
|
||||
public new string handle {
|
||||
owned get { return @"@$username@$short_instance"; }
|
||||
}
|
||||
public string short_instance {
|
||||
owned get {
|
||||
return instance
|
||||
.replace ("https://", "")
|
||||
.replace ("/","");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
public InstanceAccount () {
|
||||
on_notification.connect (show_notification);
|
||||
}
|
||||
~InstanceAccount () {
|
||||
unsubscribe ();
|
||||
}
|
||||
|
||||
public InstanceAccount.empty (string instance){
|
||||
Object (
|
||||
id: "",
|
||||
instance: instance
|
||||
);
|
||||
}
|
||||
public InstanceAccount.empty (string instance){
|
||||
Object (id: "", instance: instance);
|
||||
}
|
||||
|
||||
public InstanceAccount.from_account (API.Account account) {
|
||||
Object (
|
||||
id: account.id
|
||||
);
|
||||
patch (account);
|
||||
}
|
||||
public InstanceAccount.from_account (API.Account account) {
|
||||
Object (id: account.id);
|
||||
patch (account);
|
||||
}
|
||||
|
||||
public bool is_current () {
|
||||
return accounts.active.access_token == access_token;
|
||||
}
|
||||
public bool is_current () {
|
||||
return accounts.active.access_token == access_token;
|
||||
}
|
||||
|
||||
public string get_stream_url () {
|
||||
return @"$instance/api/v1/streaming/?stream=user&access_token=$access_token";
|
||||
}
|
||||
public string get_stream_url () {
|
||||
return @"$instance/api/v1/streaming/?stream=user&access_token=$access_token";
|
||||
}
|
||||
|
||||
public void subscribe () {
|
||||
streams.subscribe (get_stream_url (), this, out stream);
|
||||
}
|
||||
public void subscribe () {
|
||||
streams.subscribe (get_stream_url (), this, out stream);
|
||||
}
|
||||
|
||||
public void unsubscribe () {
|
||||
streams.unsubscribe (stream, this);
|
||||
}
|
||||
public void unsubscribe () {
|
||||
streams.unsubscribe (stream, this);
|
||||
}
|
||||
|
||||
protected void show_notification (API.Notification obj) {
|
||||
var title = Html.remove_tags (obj.kind.get_desc (obj.account));
|
||||
var notification = new GLib.Notification (title);
|
||||
if (obj.status != null) {
|
||||
var body = "";
|
||||
body += short_instance;
|
||||
body += "\n";
|
||||
body += Html.remove_tags (obj.status.content);
|
||||
notification.set_body (body);
|
||||
}
|
||||
public async Entity resolve (string url) throws Error {
|
||||
message (@"Resolving URL: \"$url\"...");
|
||||
var results = yield API.SearchResults.request (url, this);
|
||||
var entity = results.first ();
|
||||
message (@"Found $(entity.get_class ().get_name ())");
|
||||
return entity;
|
||||
}
|
||||
|
||||
void show_notification (API.Notification obj) {
|
||||
var title = Html.remove_tags (obj.kind.get_desc (obj.account));
|
||||
var notification = new GLib.Notification (title);
|
||||
if (obj.status != null) {
|
||||
var body = "";
|
||||
body += short_instance;
|
||||
body += "\n";
|
||||
body += Html.remove_tags (obj.status.content);
|
||||
notification.set_body (body);
|
||||
}
|
||||
|
||||
app.send_notification (app.application_id + ":" + obj.id.to_string (), notification);
|
||||
|
||||
if (obj.kind == API.NotificationType.WATCHLIST) {
|
||||
cached_notifications.add (obj);
|
||||
accounts.save ();
|
||||
}
|
||||
}
|
||||
if (obj.kind == API.NotificationType.WATCHLIST) {
|
||||
cached_notifications.add (obj);
|
||||
accounts.save ();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -142,10 +142,8 @@ public class Tootle.Views.Profile : Views.Timeline {
|
|||
return req;
|
||||
}
|
||||
|
||||
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;
|
||||
public static void open_from_id (string id) {
|
||||
var msg = new Soup.Message ("GET", @"$(accounts.active.instance)/api/v1/accounts/$id");
|
||||
network.queue (msg, (sess, mess) => {
|
||||
var node = network.parse_node (mess);
|
||||
var acc = API.Account.from (node);
|
||||
|
|
|
@ -2,111 +2,82 @@ using Gtk;
|
|||
|
||||
public class Tootle.Views.Search : Views.Base {
|
||||
|
||||
string query = "";
|
||||
SearchBar bar;
|
||||
SearchEntry entry;
|
||||
string query = "";
|
||||
SearchBar bar;
|
||||
SearchEntry entry;
|
||||
|
||||
construct {
|
||||
label = _("Search");
|
||||
construct {
|
||||
label = _("Search");
|
||||
|
||||
bar = new SearchBar ();
|
||||
bar.search_mode_enabled = true;
|
||||
bar.show ();
|
||||
pack_start (bar, false, false, 0);
|
||||
bar = new SearchBar ();
|
||||
bar.search_mode_enabled = true;
|
||||
bar.show ();
|
||||
pack_start (bar, false, false, 0);
|
||||
|
||||
entry = new SearchEntry ();
|
||||
entry.width_chars = 25;
|
||||
entry.text = query;
|
||||
entry.show ();
|
||||
bar.add (entry);
|
||||
bar.connect_entry (entry);
|
||||
entry = new SearchEntry ();
|
||||
entry.width_chars = 25;
|
||||
entry.text = query;
|
||||
entry.show ();
|
||||
bar.add (entry);
|
||||
bar.connect_entry (entry);
|
||||
|
||||
entry.activate.connect (() => request ());
|
||||
entry.icon_press.connect (() => request ());
|
||||
entry.grab_focus_without_selecting ();
|
||||
status_button.clicked.connect (request);
|
||||
entry.activate.connect (() => request ());
|
||||
entry.icon_press.connect (() => request ());
|
||||
entry.grab_focus_without_selecting ();
|
||||
status_button.clicked.connect (request);
|
||||
|
||||
request ();
|
||||
}
|
||||
request ();
|
||||
}
|
||||
|
||||
void append_account (API.Account acc) {
|
||||
var status = new API.Status.from_account (acc);
|
||||
var w = new Widgets.Status (status);
|
||||
content_list.insert (w, -1);
|
||||
on_content_changed ();
|
||||
}
|
||||
bool append (owned Entity entity) {
|
||||
var w = entity.to_widget ();
|
||||
content_list.insert (w, -1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void append_status (API.Status status) {
|
||||
var w = new Widgets.Status (status);
|
||||
content_list.insert (w, -1);
|
||||
on_content_changed ();
|
||||
}
|
||||
void append_header (string name) {
|
||||
var w = new Label (@"<span weight='bold' size='medium'>$name</span>");
|
||||
w.halign = Align.START;
|
||||
w.margin = 8;
|
||||
w.use_markup = true;
|
||||
w.show ();
|
||||
content_list.insert (w, -1);
|
||||
}
|
||||
|
||||
void append_header (string name) {
|
||||
var w = new Label (@"<span weight='bold' size='medium'>$name</span>");
|
||||
w.halign = Align.START;
|
||||
w.margin = 8;
|
||||
w.use_markup = true;
|
||||
w.show ();
|
||||
content_list.insert (w, -1);
|
||||
on_content_changed ();
|
||||
}
|
||||
void request () {
|
||||
query = entry.text.chug ().chomp ();
|
||||
if (query == "") {
|
||||
clear ();
|
||||
return;
|
||||
}
|
||||
|
||||
void append_hashtag (string name) {
|
||||
var encoded = Soup.URI.encode (name, null);
|
||||
var w = new Widgets.RichLabel (@"<a href=\"$(accounts.active.instance)/tags/$encoded\">#$name</a>");
|
||||
w.use_markup = true;
|
||||
w.halign = Align.START;
|
||||
w.margin = 8;
|
||||
w.show ();
|
||||
content_list.insert (w, -1);
|
||||
}
|
||||
clear ();
|
||||
status_message = STATUS_LOADING;
|
||||
API.SearchResults.request.begin (query, accounts.active, (obj, res) => {
|
||||
try {
|
||||
var results = API.SearchResults.request.end (res);
|
||||
|
||||
void request () {
|
||||
query = entry.text;
|
||||
if (query == "") {
|
||||
clear ();
|
||||
return;
|
||||
}
|
||||
if (!results.accounts.is_empty) {
|
||||
append_header (_("People"));
|
||||
results.accounts.@foreach (append);
|
||||
}
|
||||
|
||||
status_message = STATUS_LOADING;
|
||||
new Request.GET ("/api/v2/search")
|
||||
.with_account (accounts.active)
|
||||
.with_param ("resolve", "true")
|
||||
.with_param ("q", Soup.URI.encode (query, null))
|
||||
.then ((sess, msg) => {
|
||||
var root = network.parse (msg);
|
||||
var accounts = root.get_array_member ("accounts");
|
||||
var statuses = root.get_array_member ("statuses");
|
||||
var hashtags = root.get_array_member ("hashtags");
|
||||
if (!results.statuses.is_empty) {
|
||||
append_header (_("Posts"));
|
||||
results.statuses.@foreach (append);
|
||||
}
|
||||
|
||||
clear ();
|
||||
if (!results.hashtags.is_empty) {
|
||||
append_header (_("Hashtags"));
|
||||
results.hashtags.@foreach (append);
|
||||
}
|
||||
|
||||
if (hashtags.get_length () > 0) {
|
||||
append_header (_("Hashtags"));
|
||||
hashtags.foreach_element ((array, i, node) => {
|
||||
append_hashtag (node.get_object ().get_string_member ("name"));
|
||||
});
|
||||
}
|
||||
|
||||
if (accounts.get_length () > 0) {
|
||||
append_header (_("Accounts"));
|
||||
accounts.foreach_element ((array, i, node) => {
|
||||
var acc = API.Account.from (node);
|
||||
append_account (acc);
|
||||
});
|
||||
}
|
||||
|
||||
if (statuses.get_length () > 0) {
|
||||
append_header (_("Statuses"));
|
||||
statuses.foreach_element ((array, i, node) => {
|
||||
var status = API.Status.from (node);
|
||||
append_status (status);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on_error (on_error)
|
||||
.exec ();
|
||||
}
|
||||
on_content_changed ();
|
||||
}
|
||||
catch (Error e) {
|
||||
on_error (-1, e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -49,8 +49,8 @@ public class Tootle.Widgets.AccountsButton : Gtk.MenuButton, IAccountListener {
|
|||
|
||||
[GtkCallback]
|
||||
void open_profile () {
|
||||
Views.Profile.open_from_id (account.id);
|
||||
button.active = false;
|
||||
account.resolve_open (accounts.active);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,109 +3,85 @@ using Gee;
|
|||
|
||||
public class Tootle.Widgets.RichLabel : Label {
|
||||
|
||||
public weak ArrayList<API.Mention>? mentions;
|
||||
public weak ArrayList<API.Mention>? mentions;
|
||||
|
||||
public string text {
|
||||
get {
|
||||
return this.label;
|
||||
}
|
||||
set {
|
||||
this.label = escape_entities (Html.simplify (value));
|
||||
}
|
||||
}
|
||||
public string text {
|
||||
get {
|
||||
return this.label;
|
||||
}
|
||||
set {
|
||||
this.label = escape_entities (Html.simplify (value));
|
||||
}
|
||||
}
|
||||
|
||||
construct {
|
||||
use_markup = true;
|
||||
xalign = 0;
|
||||
wrap_mode = Pango.WrapMode.WORD_CHAR;
|
||||
justify = Justification.LEFT;
|
||||
single_line_mode = false;
|
||||
set_line_wrap (true);
|
||||
wrap_mode = Pango.WrapMode.WORD_CHAR;
|
||||
justify = Justification.LEFT;
|
||||
single_line_mode = false;
|
||||
set_line_wrap (true);
|
||||
activate_link.connect (open_link);
|
||||
}
|
||||
|
||||
public RichLabel (string text) {
|
||||
set_label (text);
|
||||
}
|
||||
public RichLabel (string text) {
|
||||
set_label (text);
|
||||
}
|
||||
|
||||
public static string escape_entities (string content) {
|
||||
return content
|
||||
.replace (" ", " ")
|
||||
.replace ("'", "'");
|
||||
}
|
||||
public static string escape_entities (string content) {
|
||||
return content
|
||||
.replace (" ", " ")
|
||||
.replace ("'", "'");
|
||||
}
|
||||
|
||||
public static string restore_entities (string content) {
|
||||
return content
|
||||
.replace ("&", "&")
|
||||
.replace ("<", "<")
|
||||
.replace (">", ">")
|
||||
.replace ("'", "'")
|
||||
.replace (""", "\"");
|
||||
}
|
||||
public static string restore_entities (string content) {
|
||||
return content
|
||||
.replace ("&", "&")
|
||||
.replace ("<", "<")
|
||||
.replace (">", ">")
|
||||
.replace ("'", "'")
|
||||
.replace (""", "\"");
|
||||
}
|
||||
|
||||
public bool open_link (string url) {
|
||||
if ("tootle://" in url)
|
||||
return false;
|
||||
public bool open_link (string url) {
|
||||
if ("tootle://" in url)
|
||||
return false;
|
||||
|
||||
if (mentions != null){
|
||||
mentions.@foreach (mention => {
|
||||
if (url == mention.url)
|
||||
Views.Profile.open_from_id (mention.id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (mentions != null){
|
||||
mentions.@foreach (mention => {
|
||||
if (url == mention.url)
|
||||
mention.open ();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if ("/tags/" in url) {
|
||||
var encoded = url.split("/tags/")[1];
|
||||
var hashtag = Soup.URI.decode (encoded);
|
||||
window.open_view (new Views.Hashtag (hashtag));
|
||||
return true;
|
||||
}
|
||||
if ("/tags/" in url) {
|
||||
var encoded = url.split ("/tags/")[1];
|
||||
var tag = Soup.URI.decode (encoded);
|
||||
window.open_view (new Views.Hashtag (tag));
|
||||
return true;
|
||||
}
|
||||
|
||||
if ("@" in url || "tags" in url) {
|
||||
new Request.GET ("/api/v2/search")
|
||||
.with_account (accounts.active)
|
||||
.with_param ("resolve", "true")
|
||||
.with_param ("q", Soup.URI.encode (url, null))
|
||||
.then ((sess, mess) => {
|
||||
var root = network.parse (mess);
|
||||
var accounts = root.get_array_member ("accounts");
|
||||
var statuses = root.get_array_member ("statuses");
|
||||
var hashtags = root.get_array_member ("hashtags");
|
||||
var resolve = "@" in url;
|
||||
var resolved = false;
|
||||
if (resolve) {
|
||||
accounts.active.resolve.begin (url, (obj, res) => {
|
||||
try {
|
||||
accounts.active.resolve.end (res).open ();
|
||||
resolved = true;
|
||||
}
|
||||
catch (Error e) {
|
||||
warning (@"Failed to resolve URL \"$url\":");
|
||||
warning (e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (accounts.get_length () > 0) {
|
||||
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 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 node = accounts.get_element (0);
|
||||
var obj = API.Tag.from (node);
|
||||
window.open_view (new Views.Hashtag (obj.name));
|
||||
}
|
||||
else {
|
||||
Desktop.open_uri (url);
|
||||
}
|
||||
})
|
||||
.on_error ((status, reason) => open_link_fallback (url, reason))
|
||||
.exec ();
|
||||
}
|
||||
else {
|
||||
Desktop.open_uri (url);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!resolved)
|
||||
Desktop.open_uri (url);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool open_link_fallback (string url, string reason) {
|
||||
warning (@"Can't resolve url: $url");
|
||||
warning (@"Reason: $reason");
|
||||
Desktop.open_uri (url);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -92,11 +92,8 @@ public class Tootle.Widgets.Status : ListBoxRow {
|
|||
public virtual signal void open () {
|
||||
if (status.id == "")
|
||||
on_avatar_clicked ();
|
||||
else {
|
||||
var formal = status.formal;
|
||||
var view = new Views.ExpandedStatus (formal);
|
||||
window.open_view (view);
|
||||
}
|
||||
else
|
||||
status.open ();
|
||||
}
|
||||
|
||||
construct {
|
||||
|
@ -183,8 +180,7 @@ public class Tootle.Widgets.Status : ListBoxRow {
|
|||
|
||||
[GtkCallback]
|
||||
public void on_avatar_clicked () {
|
||||
var view = new Views.Profile (status.formal.account);
|
||||
window.open_view (view);
|
||||
status.formal.account.open ();
|
||||
}
|
||||
|
||||
protected void open_menu () {
|
||||
|
|
|
@ -4,4 +4,11 @@ public interface Tootle.Widgetizable : GLib.Object {
|
|||
throw new Tootle.Oopsie.INTERNAL ("Widgetizable didn't provide a Widget!");
|
||||
}
|
||||
|
||||
public virtual void open () {
|
||||
warning ("Widgetizable didn't provide a way to open it!");
|
||||
}
|
||||
public virtual void resolve_open (InstanceAccount account) {
|
||||
this.open ();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
using Gtk;
|
||||
|
||||
public class Tootle.Views.Search : Views.Base {
|
||||
|
||||
string query = "";
|
||||
SearchBar bar;
|
||||
SearchEntry entry;
|
||||
|
||||
construct {
|
||||
label = _("Search");
|
||||
|
||||
bar = new SearchBar ();
|
||||
bar.search_mode_enabled = true;
|
||||
bar.show ();
|
||||
pack_start (bar, false, false, 0);
|
||||
|
||||
entry = new SearchEntry ();
|
||||
entry.width_chars = 25;
|
||||
entry.text = query;
|
||||
entry.show ();
|
||||
bar.add (entry);
|
||||
bar.connect_entry (entry);
|
||||
|
||||
entry.activate.connect (() => request ());
|
||||
entry.icon_press.connect (() => request ());
|
||||
entry.grab_focus_without_selecting ();
|
||||
status_button.clicked.connect (request);
|
||||
|
||||
request ();
|
||||
}
|
||||
|
||||
void append_account (API.Account acc) {
|
||||
var status = new API.Status.from_account (acc);
|
||||
var w = new Widgets.Status (status);
|
||||
content_list.insert (w, -1);
|
||||
on_content_changed ();
|
||||
}
|
||||
|
||||
void append_status (API.Status status) {
|
||||
var w = new Widgets.Status (status);
|
||||
content_list.insert (w, -1);
|
||||
on_content_changed ();
|
||||
}
|
||||
|
||||
void append_header (string name) {
|
||||
var w = new Label (@"<span weight='bold' size='medium'>$name</span>");
|
||||
w.halign = Align.START;
|
||||
w.margin = 8;
|
||||
w.use_markup = true;
|
||||
w.show ();
|
||||
content_list.insert (w, -1);
|
||||
on_content_changed ();
|
||||
}
|
||||
|
||||
void append_hashtag (string name) {
|
||||
var encoded = Soup.URI.encode (name, null);
|
||||
var w = new Widgets.RichLabel (@"<a href=\"$(accounts.active.instance)/tags/$encoded\">#$name</a>");
|
||||
w.use_markup = true;
|
||||
w.halign = Align.START;
|
||||
w.margin = 8;
|
||||
w.show ();
|
||||
content_list.insert (w, -1);
|
||||
}
|
||||
|
||||
void request () {
|
||||
query = entry.text;
|
||||
if (query == "") {
|
||||
clear ();
|
||||
return;
|
||||
}
|
||||
|
||||
status_message = STATUS_LOADING;
|
||||
new Request.GET ("/api/v2/search")
|
||||
.with_account (accounts.active)
|
||||
.with_param ("resolve", "true")
|
||||
.with_param ("q", Soup.URI.encode (query, null))
|
||||
.then ((sess, msg) => {
|
||||
var root = network.parse (msg);
|
||||
var accounts = root.get_array_member ("accounts");
|
||||
var statuses = root.get_array_member ("statuses");
|
||||
var hashtags = root.get_array_member ("hashtags");
|
||||
|
||||
clear ();
|
||||
|
||||
if (hashtags.get_length () > 0) {
|
||||
append_header (_("Hashtags"));
|
||||
hashtags.foreach_element ((array, i, node) => {
|
||||
append_hashtag (node.get_object ().get_string_member ("name"));
|
||||
});
|
||||
}
|
||||
|
||||
if (accounts.get_length () > 0) {
|
||||
append_header (_("Accounts"));
|
||||
accounts.foreach_element ((array, i, node) => {
|
||||
var acc = API.Account.from (node);
|
||||
append_account (acc);
|
||||
});
|
||||
}
|
||||
|
||||
if (statuses.get_length () > 0) {
|
||||
append_header (_("Statuses"));
|
||||
statuses.foreach_element ((array, i, node) => {
|
||||
var status = API.Status.from (node);
|
||||
append_status (status);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on_error (on_error)
|
||||
.exec ();
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue