1
0
mirror of https://gitlab.gnome.org/World/tootle synced 2025-02-17 03:51:11 +01:00

Merge branch 'master' into master

This commit is contained in:
Bleak Grey 2018-05-29 16:05:44 +03:00 committed by GitHub
commit d4feb6f801
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 746 additions and 575 deletions

View File

@ -1,30 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema path="/com/github/bleakgrey/tootle/" id="com.github.bleakgrey.tootle" gettext-domain="com.github.bleakgrey.tootle">
<key name="client-id" type="s">
<default>'null'</default>
<summary>Client ID</summary>
<description></description>
</key>
<key name="client-secret" type="s">
<default>'null'</default>
<summary>Client Secret</summary>
<description></description>
</key>
<key name="instance-url" type="s">
<default>'null'</default>
<summary>Instance URL</summary>
<description></description>
</key>
<key name="access-token" type="s">
<default>'null'</default>
<summary>Access Token</summary>
<description></description>
</key>
<key name="refresh-token" type="s">
<default>'null'</default>
<summary>Refresh Token</summary>
<description></description>
<key name="current-account" type="i">
<default>0</default>
<summary>Current Account</summary>
<description>Do not edit or it shall set your house on fire</description>
</key>
<key name="always-online" type="b">
<default>false</default>

View File

@ -26,6 +26,7 @@ executable(
'src/NetManager.vala',
'src/Utils.vala',
'src/Notificator.vala',
'src/InstanceAccount.vala',
'src/API/Account.vala',
'src/API/Relationship.vala',
'src/API/Mention.vala',
@ -35,7 +36,6 @@ executable(
'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/ImageToggleButton.vala',
@ -45,10 +45,10 @@ executable(
'src/Widgets/NotificationWidget.vala',
'src/Widgets/AttachmentWidget.vala',
'src/Widgets/AttachmentBox.vala',
'src/Dialogs/NewAccountDialog.vala',
'src/Dialogs/PostDialog.vala',
'src/Dialogs/SettingsDialog.vala',
'src/Views/AbstractView.vala',
'src/Views/AddAccountView.vala',
'src/Views/TimelineView.vala',
'src/Views/LocalView.vala',
'src/Views/FederatedView.vala',

View File

@ -59,7 +59,7 @@ public class Tootle.Account{
}
public Soup.Message get_relationship (){
var url = "%s/api/v1/accounts/relationships?id=%lld".printf (Tootle.settings.instance_url, id);
var url = "%s/api/v1/accounts/relationships?id=%lld".printf (Tootle.accounts.formal.instance, id);
var msg = new Soup.Message("GET", url);
msg.priority = Soup.MessagePriority.HIGH;
Tootle.network.queue (msg, (sess, mess) => {
@ -78,7 +78,7 @@ public class Tootle.Account{
public Soup.Message set_following (bool follow = true){
var action = follow ? "follow" : "unfollow";
var url = "%s/api/v1/accounts/%lld/%s".printf (Tootle.settings.instance_url, id, action);
var url = "%s/api/v1/accounts/%lld/%s".printf (Tootle.accounts.formal.instance, id, action);
var msg = new Soup.Message("POST", url);
msg.priority = Soup.MessagePriority.HIGH;
Tootle.network.queue (msg, (sess, mess) => {
@ -97,7 +97,7 @@ public class Tootle.Account{
public Soup.Message set_muted (bool mute = true){
var action = mute ? "mute" : "unmute";
var url = "%s/api/v1/accounts/%lld/%s".printf (Tootle.settings.instance_url, id, action);
var url = "%s/api/v1/accounts/%lld/%s".printf (Tootle.accounts.formal.instance, id, action);
var msg = new Soup.Message("POST", url);
msg.priority = Soup.MessagePriority.HIGH;
Tootle.network.queue (msg, (sess, mess) => {
@ -116,7 +116,7 @@ public class Tootle.Account{
public Soup.Message set_blocked (bool block = true){
var action = block ? "block" : "unblock";
var url = "%s/api/v1/accounts/%lld/%s".printf (Tootle.settings.instance_url, id, action);
var url = "%s/api/v1/accounts/%lld/%s".printf (Tootle.accounts.formal.instance, id, action);
var msg = new Soup.Message("POST", url);
msg.priority = Soup.MessagePriority.HIGH;
Tootle.network.queue (msg, (sess, mess) => {
@ -132,12 +132,5 @@ public class Tootle.Account{
});
return msg;
}
public Soup.Message get_stream () {
var url = "%s/api/v1/streaming/?stream=user&access_token=%s".printf (Tootle.settings.instance_url, Tootle.settings.access_token);
var msg = new Soup.Message("GET", url);
msg.priority = Soup.MessagePriority.VERY_HIGH;
return msg;
}
}

View File

@ -40,21 +40,21 @@ public class Tootle.Notification{
if (type == NotificationType.FOLLOW_REQUEST)
return reject_follow_request ();
var url = "%s/api/v1/notifications/dismiss?id=%lld".printf (Tootle.settings.instance_url, id);
var url = "%s/api/v1/notifications/dismiss?id=%lld".printf (Tootle.accounts.formal.instance, id);
var msg = new Soup.Message("POST", url);
Tootle.network.queue(msg);
return msg;
}
public Soup.Message accept_follow_request () {
var url = "%s/api/v1/follow_requests/%lld/authorize".printf (Tootle.settings.instance_url, account.id);
var url = "%s/api/v1/follow_requests/%lld/authorize".printf (Tootle.accounts.formal.instance, account.id);
var msg = new Soup.Message("POST", url);
Tootle.network.queue(msg);
return msg;
}
public Soup.Message reject_follow_request () {
var url = "%s/api/v1/follow_requests/%lld/reject".printf (Tootle.settings.instance_url, account.id);
var url = "%s/api/v1/follow_requests/%lld/reject".printf (Tootle.accounts.formal.instance, account.id);
var msg = new Soup.Message("POST", url);
Tootle.network.queue(msg);
return msg;

View File

@ -93,7 +93,7 @@ public class Tootle.Status {
public void set_reblogged (bool rebl = true){
var action = rebl ? "reblog" : "unreblog";
var msg = new Soup.Message("POST", "%s/api/v1/statuses/%lld/%s".printf (Tootle.settings.instance_url, id, action));
var msg = new Soup.Message("POST", "%s/api/v1/statuses/%lld/%s".printf (Tootle.accounts.formal.instance, id, action));
msg.priority = Soup.MessagePriority.HIGH;
msg.finished.connect (() => {
reblogged = rebl;
@ -108,7 +108,7 @@ public class Tootle.Status {
public void set_favorited (bool fav = true){
var action = fav ? "favourite" : "unfavourite";
var msg = new Soup.Message("POST", "%s/api/v1/statuses/%lld/%s".printf (Tootle.settings.instance_url, id, action));
var msg = new Soup.Message("POST", "%s/api/v1/statuses/%lld/%s".printf (Tootle.accounts.formal.instance, id, action));
msg.priority = Soup.MessagePriority.HIGH;
msg.finished.connect (() => {
favorited = fav;
@ -123,7 +123,7 @@ public class Tootle.Status {
public void set_muted (bool mute = true){
var action = mute ? "mute" : "unmute";
var msg = new Soup.Message("POST", "%s/api/v1/statuses/%lld/%s".printf (Tootle.settings.instance_url, id, action));
var msg = new Soup.Message("POST", "%s/api/v1/statuses/%lld/%s".printf (Tootle.accounts.formal.instance, id, action));
msg.priority = Soup.MessagePriority.HIGH;
msg.finished.connect (() => {
muted = mute;
@ -137,7 +137,7 @@ public class Tootle.Status {
}
public void poof (){
var msg = new Soup.Message("DELETE", "%s/api/v1/statuses/%lld".printf (Tootle.settings.instance_url, id));
var msg = new Soup.Message("DELETE", "%s/api/v1/statuses/%lld".printf (Tootle.accounts.formal.instance, id));
msg.priority = Soup.MessagePriority.HIGH;
msg.finished.connect (() => {
Tootle.app.toast (_("Poof!"));

View File

@ -2,118 +2,155 @@ using GLib;
public class Tootle.AccountManager : Object{
public abstract signal void switched(Account? account);
public abstract signal void added(Account account);
public abstract signal void removed(Account account);
private string dir_path;
private string file_path;
public Account? current;
public abstract signal void switched (Account? account);
public abstract signal void updated (GenericArray<InstanceAccount> accounts);
private GenericArray<InstanceAccount> saved_accounts = new GenericArray<InstanceAccount> ();
public InstanceAccount? formal {get; set;}
public Account? current {get; set;}
public AccountManager(){
Object();
dir_path = "%s/%s".printf (GLib.Environment.get_user_config_dir (), Tootle.app.application_id);
file_path = "%s/%s".printf (dir_path, "accounts.json");
}
public bool has_client_tokens(){
var client_id = Tootle.settings.client_id;
var client_secret = Tootle.settings.client_secret;
return !(client_id == "null" || client_secret == "null");
public void signal_current () {
switched (current);
updated (saved_accounts);
}
public bool has_access_token (){
return Tootle.settings.access_token != "null";
public void switch_account (int id){
debug ("Switching to account #%i", id);
Tootle.settings.current_account = id;
formal = saved_accounts.@get(id);
var msg = new Soup.Message("GET", "%s/api/v1/accounts/verify_credentials".printf (Tootle.accounts.formal.instance));
Tootle.network.queue(msg, (sess, mess) => {
try{
var root = Tootle.network.parse (mess);
current = Account.parse (root);
switched (current);
updated (saved_accounts);
}
catch (GLib.Error e) {
warning ("Can't login into %s", formal.instance);
warning (e.message);
}
});
}
public void request_auth_code (string client_id){
var pars = "?scope=read%20write%20follow";
pars += "&response_type=code";
pars += "&redirect_uri=urn:ietf:wg:oauth:2.0:oob";
pars += "&client_id=" +client_id;
public void add (InstanceAccount account) {
debug ("Adding account for %s at %s", account.username, account.instance);
saved_accounts.add (account);
save ();
updated (saved_accounts);
switch_account (saved_accounts.length - 1);
account.start_notificator ();
}
public void remove (int i) {
var account = saved_accounts.@get (i);
account.close_notificator ();
saved_accounts.remove_index (i);
if (saved_accounts.length < 1)
switched (null);
else {
var id = Tootle.settings.current_account - 1;
if (id > saved_accounts.length - 1)
id = saved_accounts.length - 1;
else if (id < saved_accounts.length - 1)
id = 0;
switch_account (id);
}
save ();
updated (saved_accounts);
if (is_empty ()) {
Tootle.window.destroy ();
NewAccountDialog.open ();
}
}
public bool is_empty () {
return saved_accounts.length == 0;
}
public void init (){
save (false);
load ();
if (saved_accounts.length < 1) {
switched (null);
NewAccountDialog.open ();
}
else
switch_account (Tootle.settings.current_account);
}
private void save (bool overwrite = true) {
try {
AppInfo.launch_default_for_uri ("%s/oauth/authorize%s".printf (Tootle.settings.instance_url, pars), null);
var dir = File.new_for_path (dir_path);
if (!dir.query_exists ())
dir.make_directory ();
var file = File.new_for_path (file_path);
if (file.query_exists () && !overwrite)
return;
var builder = new Json.Builder ();
builder.begin_array ();
saved_accounts.foreach ((acc) => {
var node = acc.serialize ();
builder.add_value (node);
});
builder.end_array ();
var generator = new Json.Generator ();
generator.set_root (builder.get_root ());
var data = generator.to_data (null);
if (file.query_exists ())
file.@delete ();
FileOutputStream stream = file.create (FileCreateFlags.PRIVATE);
stream.write (data.data);
}
catch (GLib.Error e){
warning (e.message);
}
}
private void load () {
try {
uint8[] data;
string etag;
var file = File.new_for_path (file_path);
file.load_contents (null, out data, out etag);
var contents = (string) data;
var parser = new Json.Parser ();
parser.load_from_data (contents, -1);
var array = parser.get_root ().get_array ();
saved_accounts = new GenericArray<InstanceAccount> ();
array.foreach_element ((_arr, _i, node) => {
var obj = node.get_object ();
var account = InstanceAccount.parse (obj);
if (account != null) {
saved_accounts.add (account);
account.start_notificator ();
}
});
debug ("Loaded %i saved accounts", saved_accounts.length);
updated (saved_accounts);
}
catch (GLib.Error e){
warning (e.message);
}
}
public Soup.Message request_client_tokens(){
var pars = "?client_name=Tootle";
pars += "&redirect_uris=urn:ietf:wg:oauth:2.0:oob";
pars += "&website=https://github.com/bleakgrey/tootle";
pars += "&scopes=read%20write%20follow";
var msg = new Soup.Message("POST", "%s/api/v1/apps%s".printf (Tootle.settings.instance_url, pars));
Tootle.network.queue(msg, (sess, mess) => {
try{
var root = Tootle.network.parse (mess);
var client_id = root.get_string_member ("client_id");
var client_secret = root.get_string_member ("client_secret");
Tootle.settings.client_id = client_id;
Tootle.settings.client_secret = client_secret;
debug ("Received tokens");
request_auth_code (client_id);
}
catch (GLib.Error e) {
warning ("Can't request client secret.");
warning (e.message);
}
});
return msg;
}
public Soup.Message try_auth (string code){
var pars = "?client_id=" + Tootle.settings.client_id;
pars += "&client_secret=" + Tootle.settings.client_secret;
pars += "&redirect_uri=urn:ietf:wg:oauth:2.0:oob";
pars += "&grant_type=authorization_code";
pars += "&code=" + code;
var msg = new Soup.Message("POST", "%s/oauth/token%s".printf (Tootle.settings.instance_url, pars));
Tootle.network.queue(msg, (sess, mess) => {
try{
var root = Tootle.network.parse (mess);
var access_token = root.get_string_member ("access_token");
Tootle.settings.access_token = access_token;
debug ("Got access token");
request_current ();
}
catch (GLib.Error e) {
warning ("Can't get access token");
warning (e.message);
}
});
return msg;
}
public Soup.Message request_current (){
var msg = new Soup.Message("GET", "%s/api/v1/accounts/verify_credentials".printf (Tootle.settings.instance_url));
Tootle.network.queue(msg, (sess, mess) => {
try{
var root = Tootle.network.parse (mess);
current = Account.parse(root);
switched (current);
}
catch (GLib.Error e) {
warning ("Can't get current user");
warning (e.message);
}
});
return msg;
}
public void logout (){
current = null;
Tootle.settings.access_token = "null";
switched (null);
}
public void init (){
if(has_access_token())
request_current ();
else
switched (null);
}
}

View File

@ -33,7 +33,9 @@ namespace Tootle{
accounts = new AccountManager ();
network = new NetManager ();
image_cache = new ImageCache ();
accounts.init ();
app.error.connect (app.on_error);
return app.run (args);
}
@ -47,14 +49,28 @@ namespace Tootle{
protected override void activate () {
if (window != null) {
debug ("Reopening window");
window.present ();
if (!accounts.is_empty ())
window.present ();
else
NewAccountDialog.open ();
}
else {
debug ("Creating new window");
window = new MainWindow (this);
window.present ();
if (accounts.is_empty ())
NewAccountDialog.open ();
else {
window = new MainWindow (this);
window.present ();
}
}
}
protected void on_error (string title, string msg){
var message_dialog = new Granite.MessageDialog.with_image_from_icon_name (title, msg, "dialog-warning");
message_dialog.transient_for = window;
message_dialog.run ();
message_dialog.destroy ();
}
}

View File

@ -0,0 +1,218 @@
using Gtk;
using Tootle;
public class Tootle.NewAccountDialog : Gtk.Dialog {
private static NewAccountDialog dialog;
private Gtk.Grid grid;
private Gtk.Button button_done;
private Gtk.Image logo;
private Gtk.Entry instance_entry;
private Gtk.Label instance_register;
private Gtk.Label code_name;
private Gtk.Entry code_entry;
private string? instance;
private string? client_id;
private string? client_secret;
private string? code;
private string? token;
private string? username;
public NewAccountDialog () {
Object (
border_width: 6,
deletable: true,
resizable: false,
title: _("New Account"),
transient_for: Tootle.window
);
logo = new Image.from_resource ("/com/github/bleakgrey/tootle/logo128");
logo.halign = Gtk.Align.CENTER;
logo.hexpand = true;
logo.margin_bottom = 24;
instance_entry = new Entry ();
instance_entry.text = "https://myinstance.com";
instance_entry.set_placeholder_text ("https://myinstance.com");
instance_entry.width_chars = 30;
instance_register = new Label ("<a href=\"https://joinmastodon.org/\">%s</a>".printf (_("What's an instance?")));
instance_register.halign = Gtk.Align.END;
instance_register.set_use_markup (true);
code_name = new AlignedLabel (_("Code:"));
code_entry = new Entry ();
code_entry.secondary_icon_name = "dialog-question-symbolic";
code_entry.secondary_icon_tooltip_text = _("Paste your instance authorization code here");
code_entry.secondary_icon_activatable = false;
button_done = new Gtk.Button.with_label (_("Add Account"));
button_done.clicked.connect (on_done_clicked);
button_done.halign = Gtk.Align.END;
button_done.margin_top = 24;
grid = new Gtk.Grid ();
grid.column_spacing = 12;
grid.row_spacing = 6;
grid.hexpand = true;
grid.halign = Gtk.Align.CENTER;
grid.attach (logo, 0, 0, 2, 1);
grid.attach (new AlignedLabel (_("Instance:")), 0, 1);
grid.attach (instance_entry, 1, 1);
grid.attach (code_name, 0, 3);
grid.attach (code_entry, 1, 3);
grid.attach (instance_register, 1, 5);
grid.attach (button_done, 1, 10);
var content = get_content_area () as Gtk.Box;
content.pack_start (grid, false, false, 0);
destroy.connect (() => {
dialog = null;
if (Tootle.accounts.is_empty ())
Tootle.app.remove_window (Tootle.window_dummy);
});
show_all ();
clear ();
}
private void clear () {
code_name.hide ();
code_entry.hide ();
code_entry.text = "";
client_id = client_secret = code = token = null;
}
private void on_done_clicked () {
instance = instance_entry.text;
code = code_entry.text;
if (this.client_id == null || this.client_secret == null) {
request_client_tokens ();
return;
}
if (code == "")
Tootle.app.error (_("Error"), _("Please paste valid instance authorization code"));
else
try_auth (code);
}
private bool show_error (Soup.Message msg) {
if (msg.status_code != Soup.Status.OK) {
var phrase = Soup.Status.get_phrase (msg.status_code);
Tootle.app.error (_("Network Error"), phrase);
return true;
}
return false;
}
private void request_client_tokens (){
var pars = "?client_name=Tootle";
pars += "&redirect_uris=urn:ietf:wg:oauth:2.0:oob";
pars += "&website=https://github.com/bleakgrey/tootle";
pars += "&scopes=read%20write%20follow";
grid.sensitive = false;
var msg = new Soup.Message ("POST", "%s/api/v1/apps%s".printf (instance, pars));
Tootle.network.queue_custom (msg, (sess, mess) => {
grid.sensitive = true;
if (show_error (mess)) return;
var root = Tootle.network.parse (mess);
var id = root.get_string_member ("client_id");
var secret = root.get_string_member ("client_secret");
client_id = id;
client_secret = secret;
info ("Received tokens from %s", instance);
request_auth_code ();
code_name.show ();
code_entry.show ();
});
}
private void request_auth_code (){
var pars = "?scope=read%20write%20follow";
pars += "&response_type=code";
pars += "&redirect_uri=urn:ietf:wg:oauth:2.0:oob";
pars += "&client_id=" + client_id;
try {
info ("Requesting auth token");
AppInfo.launch_default_for_uri ("%s/oauth/authorize%s".printf (instance, pars), null);
}
catch (GLib.Error e){
warning (e.message);
Tootle.app.error (_("Error"), e.message);
}
}
private void try_auth (string code){
var pars = "?client_id=" + client_id;
pars += "&client_secret=" + client_secret;
pars += "&redirect_uri=urn:ietf:wg:oauth:2.0:oob";
pars += "&grant_type=authorization_code";
pars += "&code=" + code;
var msg = new Soup.Message ("POST", "%s/oauth/token%s".printf (instance, pars));
Tootle.network.queue_custom (msg, (sess, mess) => {
try{
if (show_error (mess)) return;
var root = Tootle.network.parse (mess);
token = root.get_string_member ("access_token");
debug ("Got access token");
get_username ();
}
catch (GLib.Error e) {
warning ("Can't get access token");
warning (e.message);
}
});
}
private void get_username () {
var msg = new Soup.Message("GET", "%s/api/v1/accounts/verify_credentials".printf (instance));
msg.request_headers.append ("Authorization", "Bearer " + token);
Tootle.network.queue_custom (msg, (sess, mess) => {
try{
if (show_error (mess)) return;
var root = Tootle.network.parse (mess);
username = root.get_string_member ("username");
add_account ();
Tootle.window.show ();
Tootle.window.present ();
destroy ();
}
catch (GLib.Error e) {
warning ("Can't get username");
warning (e.message);
}
});
}
private void add_account () {
var account = new InstanceAccount ();
account.username = username;
account.instance = instance;
account.client_id = client_id;
account.client_secret = client_secret;
account.token = token;
Tootle.accounts.add (account);
Tootle.app.activate ();
}
public static void open () {
if (dialog == null)
dialog = new NewAccountDialog ();
}
}

View File

@ -184,7 +184,7 @@ public class Tootle.PostDialog : Gtk.Dialog {
pars += "&spoiler_text=" + Soup.URI.encode (spoiler_text.buffer.text, null);
}
var url = "%s/api/v1/statuses%s".printf (Tootle.settings.instance_url, pars);
var url = "%s/api/v1/statuses%s".printf (Tootle.accounts.formal.instance, pars);
var msg = new Soup.Message("POST", url);
Tootle.network.queue(msg, (sess, mess) => {
try {

View File

@ -27,13 +27,13 @@ public class Tootle.SettingsDialog : Gtk.Dialog {
grid.attach (new SettingsLabel (_("Real-time updates:")), 0, i);
grid.attach (new SettingsSwitch ("live-updates"), 1, i++);
grid.attach (new Granite.HeaderLabel (_("Caching")), 0, i++, 2, 1);
grid.attach (new SettingsLabel (_("Use cache:")), 0, i);
grid.attach (new SettingsSwitch ("cache"), 1, i++);
grid.attach (new SettingsLabel (_("Max cache size (MB):")), 0, i);
var cache_size = new Gtk.SpinButton.with_range (16, 256, 1);
settings.schema.bind ("cache-size", cache_size, "value", SettingsBindFlags.DEFAULT);
grid.attach (cache_size, 1, i++);
// grid.attach (new Granite.HeaderLabel (_("Caching")), 0, i++, 2, 1);
// grid.attach (new SettingsLabel (_("Use cache:")), 0, i);
// grid.attach (new SettingsSwitch ("cache"), 1, i++);
// grid.attach (new SettingsLabel (_("Max cache size (MB):")), 0, i);
// var cache_size = new Gtk.SpinButton.with_range (16, 256, 1);
// settings.schema.bind ("cache-size", cache_size, "value", SettingsBindFlags.DEFAULT);
// grid.attach (cache_size, 1, i++);
grid.attach (new Granite.HeaderLabel (_("Notifications")), 0, i++, 2, 1);
grid.attach (new SettingsLabel (_("Always receive notifications:")), 0, i);

99
src/InstanceAccount.vala Normal file
View File

@ -0,0 +1,99 @@
public class Tootle.InstanceAccount : GLib.Object {
public string username {get; set;}
public string instance {get; set;}
public string client_id {get; set;}
public string client_secret {get; set;}
public string token {get; set;}
private Notificator? notificator;
public InstanceAccount (){
Object ();
}
public string get_pretty_instance () {
return instance
.replace ("https://", "")
.replace ("/","");
}
public void start_notificator () {
if (notificator != null)
notificator.close ();
notificator = new Notificator (get_stream ());
notificator.status_added.connect (status_added);
notificator.status_removed.connect (status_removed);
notificator.notification.connect (notification);
notificator.start ();
}
private Soup.Message get_stream () {
var url = "%s/api/v1/streaming/?stream=user&access_token=%s".printf (instance, token);
var msg = new Soup.Message("GET", url);
return msg;
}
public void close_notificator () {
if (notificator != null)
notificator.close ();
}
public 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 (this.username);
builder.set_member_name ("instance");
builder.add_string_value (this.instance);
builder.set_member_name ("id");
builder.add_string_value (this.client_id);
builder.set_member_name ("secret");
builder.add_string_value (this.client_secret);
builder.set_member_name ("token");
builder.add_string_value (this.token);
builder.end_object ();
return builder.get_root ();
}
public static InstanceAccount parse (Json.Object obj) {
var acc = new InstanceAccount ();
acc.username = obj.get_string_member ("username");
acc.instance = obj.get_string_member ("instance");
acc.client_id = obj.get_string_member ("id");
acc.client_secret = obj.get_string_member ("secret");
acc.token = obj.get_string_member ("token");
return acc;
}
private void notification (ref Notification obj) {
var title = Utils.escape_html (obj.type.get_desc (obj.account));
var notification = new GLib.Notification (title);
if (obj.status != null) {
var body = "";
body += get_pretty_instance ();
body += "\n";
body += Utils.escape_html (obj.status.content);
notification.set_body (body);
}
app.send_notification (app.application_id + ":" + obj.id.to_string (), notification);
network.notification (ref obj);
}
private void status_added (ref Status status) {
if (settings.live_updates)
network.status_added (ref status, "home");
else
app.toast (_("New toot available"));
}
private void status_removed (int64 id) {
if (settings.live_updates)
network.status_removed (id);
}
}

View File

@ -1,19 +1,21 @@
using Gtk;
public class Tootle.MainWindow: Gtk.Window {
private weak SettingsManager settings;
private Gtk.Overlay overlay;
private Granite.Widgets.Toast toast;
private Gtk.Grid grid;
public Tootle.HeaderBar header;
public Stack primary_stack;
public Stack secondary_stack;
private Stack primary_stack;
private Stack secondary_stack;
public Gtk.HeaderBar header;
private Granite.Widgets.ModeButton button_mode;
private AccountsButton button_accounts;
private Spinner spinner;
private Button button_toot;
private Button button_back;
construct {
settings = Tootle.settings;
var provider = new Gtk.CssProvider ();
provider.load_from_resource ("/com/github/bleakgrey/tootle/app.css");
StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
@ -33,15 +35,54 @@ public class Tootle.MainWindow: Gtk.Window {
primary_stack.add_named (secondary_stack, "0");
primary_stack.hexpand = true;
primary_stack.vexpand = true;
header = new Tootle.HeaderBar ();
spinner = new Spinner ();
spinner.active = true;
button_accounts = new AccountsButton ();
button_back = new Button ();
button_back.label = _("Back");
button_back.get_style_context ().add_class (Granite.STYLE_CLASS_BACK_BUTTON);
button_back.clicked.connect (() => back ());
button_toot = new Button ();
button_toot.tooltip_text = _("Toot");
button_toot.image = new Gtk.Image.from_icon_name ("document-edit-symbolic", Gtk.IconSize.LARGE_TOOLBAR);
button_toot.clicked.connect (() => PostDialog.open ());
button_mode = new Granite.Widgets.ModeButton ();
button_mode.get_style_context ().add_class ("mode");
button_mode.mode_changed.connect (widget => {
secondary_stack.set_visible_child_name (widget.tooltip_text);
});
button_mode.show ();
header = new Gtk.HeaderBar ();
header.show_close_button = true;
header.custom_title = button_mode;
header.show_all ();
header.pack_start (button_back);
header.pack_start (button_toot);
header.pack_end (button_accounts);
header.pack_end (spinner);
grid = new Gtk.Grid ();
grid.set_size_request (400, 500);
grid.attach (primary_stack, 0, 0, 1, 1);
grid.attach (overlay, 0, 0, 1, 1);
add_header_view (new TimelineView ("home"));
add_header_view (new NotificationsView ());
add_header_view (new LocalView ());
add_header_view (new FederatedView ());
button_mode.set_active (0);
update_header ();
add (grid);
show_all ();
button_mode.valign = Gtk.Align.FILL;
}
public MainWindow (Gtk.Application application) {
@ -53,43 +94,16 @@ public class Tootle.MainWindow: Gtk.Window {
set_titlebar (header);
window_position = WindowPosition.CENTER;
Tootle.accounts.switched.connect(on_account_switched);
Tootle.app.error.connect (on_error);
Tootle.app.toast.connect (on_toast);
Tootle.accounts.init ();
}
private void reset () {
header.button_mode.clear_children ();
secondary_stack.forall (widget => widget.destroy ());
}
private void on_account_switched(Account? account = Tootle.accounts.current){
reset ();
if(account == null)
build_setup_view ();
else
build_main_view ();
}
private void build_setup_view (){
var add_account = new AddAccountView ();
secondary_stack.add_named (add_account, add_account.get_name ());
header.update (false, true);
}
private void build_main_view (){
add_header_view (new TimelineView ("home"));
add_header_view (new NotificationsView ());
add_header_view (new LocalView ());
add_header_view (new FederatedView ());
header.update (true);
app.toast.connect (on_toast);
network.started.connect (() => spinner.show ());
network.finished.connect (() => spinner.hide ());
accounts.signal_current ();
}
private void add_header_view (AbstractView view) {
var img = new Gtk.Image.from_icon_name(view.get_icon (), Gtk.IconSize.LARGE_TOOLBAR);
img.tooltip_text = view.get_name ();
header.button_mode.append (img);
button_mode.append (img);
view.image = img;
secondary_stack.add_named(view, view.get_name ());
@ -97,13 +111,34 @@ public class Tootle.MainWindow: Gtk.Window {
img.pixel_size = 20; // For some reason Notifications icon is too small without this
}
public void open_view (Widget widget) {
widget.show ();
var i = int.parse (primary_stack.get_visible_child_name ());
public int get_visible_id () {
return int.parse (primary_stack.get_visible_child_name ());
}
public void open_view (AbstractView widget) {
var i = get_visible_id ();
i++;
widget.stack_pos = i;
widget.show ();
primary_stack.add_named (widget, i.to_string ());
primary_stack.set_visible_child_name (i.to_string ());
header.update (false);
update_header ();
}
public void back () {
var i = get_visible_id ();
var child = primary_stack.get_child_by_name (i.to_string ());
primary_stack.set_visible_child_name ((i-1).to_string ());
child.destroy ();
update_header ();
}
public void reopen_view (int view_id) {
var i = get_visible_id ();
while (i != view_id && view_id != 0) {
back ();
i = get_visible_id ();
}
}
private void on_toast (string msg){
@ -111,16 +146,9 @@ public class Tootle.MainWindow: Gtk.Window {
toast.send_notification ();
}
private void on_error (string title, string msg){
var message_dialog = new Granite.MessageDialog.with_image_from_icon_name (title, msg, "dialog-warning");
message_dialog.transient_for = this;
message_dialog.run ();
message_dialog.destroy ();
}
public override bool delete_event (Gdk.EventAny event) {
this.destroy.connect (() => {
if (!Tootle.settings.always_online)
if (!Tootle.settings.always_online || Tootle.accounts.is_empty ())
Tootle.app.remove_window (Tootle.window_dummy);
Tootle.window = null;
});
@ -133,8 +161,15 @@ public class Tootle.MainWindow: Gtk.Window {
var theme = is_dark ? "dark" : "light";
provider.load_from_resource ("/com/github/bleakgrey/tootle/%s.css".printf (theme));
StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
Gtk.Settings.get_default ().gtk_application_prefer_dark_theme = is_dark;
}
private void update_header () {
bool primary_mode = get_visible_id () == 0;
button_mode.set_visible (primary_mode);
button_toot.set_visible (primary_mode);
button_back.set_visible (!primary_mode);
button_accounts.set_visible (true);
}
}

View File

@ -17,8 +17,6 @@ public class Tootle.NetManager : GLib.Object {
private Soup.Cache cache;
public string cache_path;
private Notificator? notificator;
construct {
cache_path = "%s/%s".printf (GLib.Environment.get_user_cache_dir (), Tootle.app.application_id);
cache = new Soup.Cache (cache_path, Soup.CacheType.SINGLE_USER);
@ -45,38 +43,23 @@ public class Tootle.NetManager : GLib.Object {
public NetManager() {
GLib.Object();
Tootle.accounts.switched.connect (acc => {
if (notificator != null)
notificator.close ();
if (acc == null)
return;
notificator = new Notificator (acc);
notificator.start ();
});
}
private void on_settings_changed () {
cache.set_max_size (1024 * 1024 * Tootle.settings.cache_size);
var has_cache = session.has_feature (cache.get_type ());
if (Tootle.settings.cache) {
if (!has_cache) {
debug ("Turning on cache");
session.add_feature (cache);
}
}
else {
if (has_cache) {
debug ("Turning off cache");
session.remove_feature (cache);
}
}
}
public void abort (Soup.Message msg) {
session.cancel_message (msg, 0);
// cache.set_max_size (1024 * 1024 * Tootle.settings.cache_size);
// var has_cache = session.has_feature (cache.get_type ());
// if (Tootle.settings.cache) {
// if (!has_cache) {
// debug ("Turning on cache");
// session.add_feature (cache);
// }
// }
// else {
// if (has_cache) {
// debug ("Turning off cache");
// session.remove_feature (cache);
// }
// }
}
public async WebsocketConnection stream (Soup.Message msg) {
@ -87,9 +70,9 @@ public class Tootle.NetManager : GLib.Object {
requests_processing++;
started ();
var token = Tootle.settings.access_token;
if(token != "null")
msg.request_headers.append ("Authorization", "Bearer " + token);
var formal = Tootle.accounts.formal;
if(formal != null)
msg.request_headers.append ("Authorization", "Bearer " + formal.token);
session.queue_message (msg, (sess, mess) => {
switch (mess.tls_errors){
@ -108,6 +91,12 @@ public class Tootle.NetManager : GLib.Object {
break;
}
if (mess.status_code != Soup.Status.OK) {
var phrase = Soup.Status.get_phrase (mess.status_code);
Tootle.app.toast (_("Error: %s").printf(phrase));
return;
}
if (cb != null)
cb (sess, mess);
@ -119,6 +108,10 @@ public class Tootle.NetManager : GLib.Object {
return msg;
}
public void queue_custom (Soup.Message msg, owned Soup.SessionCallback cb) {
session.queue_message (msg, cb);
}
public Json.Object parse (Soup.Message msg) throws GLib.Error {
// debug ("Status Code: %u", msg.status_code);
// debug ("Message length: %lld", msg.response_body.length);

View File

@ -3,24 +3,28 @@ using Soup;
public class Tootle.Notificator : GLib.Object {
weak Account account;
WebsocketConnection? connection;
Soup.Message msg;
public Notificator (Account acc){
public abstract signal void notification (ref Notification notification);
public abstract signal void status_added (ref Status status);
public abstract signal void status_removed (int64 id);
public Notificator (Soup.Message msg){
Object ();
account = acc;
this.msg = msg;
this.msg.priority = Soup.MessagePriority.VERY_HIGH;
}
public async void start () {
var msg = account.get_stream ();
debug ("Starting notificator");
connection = yield Tootle.network.stream (msg);
connection.error.connect (on_error);
connection.message.connect (on_message);
debug ("Receiving notifications for %lld", account.id);
}
public void close () {
debug ("Closing notifications for %lld", account.id);
debug ("Stopping notificator");
connection.close (0, null);
}
@ -29,7 +33,6 @@ public class Tootle.Notificator : GLib.Object {
}
private void on_message (int i, Bytes bytes) {
var network = Tootle.network;
var msg = (string) bytes.get_data ();
var parser = new Json.Parser ();
@ -39,32 +42,21 @@ public class Tootle.Notificator : GLib.Object {
var type = root.get_string_member ("event");
switch (type) {
case "update":
if (Tootle.settings.live_updates) {
var status = Status.parse (sanitize (root));
network.status_added (ref status, "home");
}
else
Tootle.app.toast ("New post available");
var status = Status.parse (sanitize (root));
status_added (ref status);
break;
case "delete":
if (Tootle.settings.live_updates) {
var id = int64.parse (root.get_string_member("payload"));
network.status_removed (id);
}
var id = int64.parse (root.get_string_member("payload"));
status_removed (id);
break;
case "notification":
var notif = Notification.parse (sanitize (root));
toast (notif);
network.notification (ref notif);
notification (ref notif);
break;
default:
warning ("Unknown push event: %s", type);
break;
}
}
private Json.Object sanitize (Json.Object root) {
@ -75,12 +67,4 @@ public class Tootle.Notificator : GLib.Object {
return parser.get_root ().get_object ();
}
private void toast (Notification obj) {
var title = Utils.escape_html (obj.type.get_desc (obj.account));
var notification = new GLib.Notification (title);
if (obj.status != null)
notification.set_body (Utils.escape_html (obj.status.content));
Tootle.app.send_notification (Tootle.app.application_id + ":" + obj.id.to_string (), notification);
}
}

View File

@ -1,25 +1,12 @@
public class Tootle.SettingsManager : Granite.Services.Settings {
public string client_id { get; set; }
public string client_secret { get; set; }
public string access_token { get; set; }
public string refresh_token { get; set; }
public string instance_url { get; set; }
public int current_account { get; set; }
public bool always_online { get; set; }
public bool cache { get; set; }
public int cache_size { get; set; }
public bool live_updates { get; set; }
public bool dark_theme { get; set; }
public void clear_account (){
access_token = "null";
refresh_token = "null";
instance_url = "null";
client_id = "null";
client_secret = "null";
debug ("Removed current account");
}
public SettingsManager () {
base ("com.github.bleakgrey.tootle");
}

View File

@ -2,6 +2,7 @@ using Gtk;
public abstract class Tootle.AbstractView : Gtk.ScrolledWindow {
public int stack_pos = -1;
public Gtk.Image? image;
public Gtk.Box view;
protected Gtk.Box? empty;

View File

@ -209,7 +209,7 @@ public class Tootle.AccountView : TimelineView {
if (page_next != null)
return page_next;
var url = "%s/api/v1/accounts/%lld/statuses?limit=%i".printf (Tootle.settings.instance_url, account.id, this.limit);
var url = "%s/api/v1/accounts/%lld/statuses?limit=%i".printf (Tootle.accounts.formal.instance, account.id, this.limit);
return url;
}
@ -232,7 +232,7 @@ public class Tootle.AccountView : TimelineView {
}
public static void open_from_id (int64 id){
var url = "%s/api/v1/accounts/%lld".printf (Tootle.settings.instance_url, id);
var url = "%s/api/v1/accounts/%lld".printf (Tootle.accounts.formal.instance, id);
var msg = new Soup.Message("GET", url);
msg.priority = Soup.MessagePriority.HIGH;
Tootle.network.queue(msg, (sess, mess) => {
@ -249,7 +249,7 @@ public class Tootle.AccountView : TimelineView {
}
public static void open_from_name (string name){
var url = "%s/api/v1/accounts/search?limit=1&q=%s".printf (Tootle.settings.instance_url, name);
var url = "%s/api/v1/accounts/search?limit=1&q=%s".printf (Tootle.accounts.formal.instance, name);
var msg = new Soup.Message("GET", url);
msg.priority = Soup.MessagePriority.HIGH;
Tootle.network.queue(msg, (sess, mess) => {

View File

@ -1,146 +0,0 @@
using Gtk;
using Granite;
public class Tootle.AddAccountView : Tootle.AbstractView {
public Stack stack;
GridInstance grid_instance;
GridCode grid_code;
protected class GridInstance : Grid{
Image image;
public Button button_next;
public Entry entry;
construct{
column_spacing = 12;
row_spacing = 6;
hexpand = true;
halign = Gtk.Align.CENTER;
image = new Image.from_resource ("/com/github/bleakgrey/tootle/logo128");
image.halign = Gtk.Align.CENTER;
image.hexpand = true;
image.margin_bottom = 24;
entry = new Entry ();
entry.text = "https://myinstance.com";
entry.set_placeholder_text ("https://myinstance.com");
entry.width_chars = 30;
button_next = new Button.with_label ("Next");
button_next.halign = Gtk.Align.END;
var register = new Label ("<a href=\"https://joinmastodon.org/\">%s</a>".printf (_("What's an instance?")));
register.halign = Gtk.Align.END;
register.set_use_markup (true);
attach (image, 0, 1, 2, 1);
attach (new AlignedLabel (_("Instance:")), 0, 2, 1, 1);
attach (entry, 1, 2, 1, 1);
attach (button_next, 0, 3, 2, 1);
attach (register, 0, 4, 2, 1);
}
public GridInstance(){}
}
protected class GridCode : Grid{
Granite.Widgets.Avatar image;
public Button button_back;
public Button button_next;
public Entry code;
construct{
column_spacing = 12;
row_spacing = 6;
hexpand = true;
halign = Gtk.Align.CENTER;
valign = Gtk.Align.CENTER;
image = new Granite.Widgets.Avatar.with_default_icon (128);
image.halign = Gtk.Align.CENTER;
image.hexpand = true;
image.margin_bottom = 24;
code = new Entry ();
code.width_chars = 30;
button_next = new Button.with_label (_("Add Account"));
button_next.halign = Gtk.Align.END;
button_back = new Button.with_label (_("Back"));
button_back.halign = Gtk.Align.START;
attach (image, 0, 1, 2, 1);
attach (new AlignedLabel (_("Authorization Code:")), 0, 2, 1, 1);
attach (code, 1, 2, 1, 1);
attach (button_back, 0, 3, 1, 1);
attach (button_next, 1, 3, 1, 1);
}
public GridCode(){}
}
construct {
view.valign = Gtk.Align.CENTER;
stack = new Stack ();
stack.valign = Gtk.Align.CENTER;
stack.vexpand = true;
stack.transition_type = StackTransitionType.SLIDE_LEFT_RIGHT;
grid_instance = new GridInstance ();
grid_instance.button_next.clicked.connect(on_next_click);
grid_code = new GridCode ();
grid_code.button_back.clicked.connect(() => stack.set_visible_child_name ("instance"));
grid_code.button_next.clicked.connect(on_add_click);
var header1 = new Gtk.Label (_("Enter Your Instance URL:"));
header1.get_style_context ().add_class (Granite.STYLE_CLASS_H2_LABEL);
header1.halign = Gtk.Align.CENTER;
header1.hexpand = true;
stack.add_named (grid_instance, "instance");
stack.add_named (grid_code, "code");
view.add (stack);
show_all ();
}
public AddAccountView () {
base ();
}
public override string get_name () {
return "add_account";
}
private void on_next_click(){
Tootle.settings.clear_account ();
Tootle.settings.instance_url = grid_instance.entry.text;
grid_instance.sensitive = false;
if(!Tootle.accounts.has_client_tokens ()){
var msg = Tootle.accounts.request_client_tokens ();
msg.finished.connect(() => {
grid_instance.sensitive = true;
stack.set_visible_child_name ("code");
});
}
else{
grid_instance.sensitive = true;
stack.set_visible_child_name ("code");
Tootle.accounts.request_auth_code (Tootle.settings.client_id);
}
}
private void on_add_click (){
var code = grid_code.code.text;
Tootle.accounts.try_auth (code);
}
}

View File

@ -11,7 +11,7 @@ public class Tootle.FavoritesView : TimelineView {
if (page_next != null)
return page_next;
var url = "%s/api/v1/favourites/?limit=%i".printf (Tootle.settings.instance_url, this.limit);
var url = "%s/api/v1/favourites/?limit=%i".printf (Tootle.accounts.formal.instance, this.limit);
return url;
}

View File

@ -6,7 +6,7 @@ public class Tootle.FollowersView : TimelineView {
base (account.id.to_string ());
}
public new void prepend (ref Account account){
public new void append (ref Account account){
if (empty != null)
empty.destroy ();
@ -23,7 +23,7 @@ public class Tootle.FollowersView : TimelineView {
if (page_next != null)
return page_next;
var url = "%s/api/v1/accounts/%s/followers".printf (Tootle.settings.instance_url, this.timeline);
var url = "%s/api/v1/accounts/%s/followers".printf (Tootle.accounts.formal.instance, this.timeline);
return url;
}
@ -36,7 +36,7 @@ public class Tootle.FollowersView : TimelineView {
var object = node.get_object ();
if (object != null){
var status = Account.parse (object);
prepend (ref status);
append (ref status);
}
});

View File

@ -11,7 +11,7 @@ public class Tootle.FollowingView : FollowersView {
if (page_next != null)
return page_next;
var url = "%s/api/v1/accounts/%s/following".printf (Tootle.settings.instance_url, this.timeline);
var url = "%s/api/v1/accounts/%s/following".printf (Tootle.accounts.formal.instance, this.timeline);
return url;
}

View File

@ -7,7 +7,7 @@ public class Tootle.LocalView : TimelineView {
}
public override string get_icon () {
return "folder-recent-symbolic";
return "document-open-recent-symbolic";
}
public override string get_name () {

View File

@ -73,7 +73,7 @@ public class Tootle.NotificationsView : AbstractView {
}
public void request () {
var url = "%s/api/v1/follow_requests".printf (Tootle.settings.instance_url);
var url = "%s/api/v1/follow_requests".printf (Tootle.accounts.formal.instance);
var msg = new Soup.Message("GET", url);
Tootle.network.queue(msg, (sess, mess) => {
try{
@ -91,7 +91,7 @@ public class Tootle.NotificationsView : AbstractView {
}
});
var url2 = "%s/api/v1/notifications?limit=30".printf (Tootle.settings.instance_url);
var url2 = "%s/api/v1/notifications?limit=30".printf (Tootle.accounts.formal.instance);
var msg2 = new Soup.Message("GET", url2);
Tootle.network.queue(msg2, (sess, mess) => {
try{

View File

@ -2,7 +2,8 @@ using Gtk;
public class Tootle.SearchView : AbstractView {
Gtk.Entry entry;
private string query = "";
private Gtk.Entry entry;
construct {
view.margin_bottom = 6;
@ -11,6 +12,7 @@ public class Tootle.SearchView : AbstractView {
entry.placeholder_text = _("Search");
entry.secondary_icon_name = "system-search-symbolic";
entry.width_chars = 25;
entry.text = query;
entry.show ();
Tootle.window.header.pack_start (entry);
@ -45,7 +47,7 @@ public class Tootle.SearchView : AbstractView {
}
private void append_hashtag (string name) {
var text = "<a href=\"%s/tags/%s\">#%s</a>".printf (Tootle.settings.instance_url, Soup.URI.encode (name, null), name);
var text = "<a href=\"%s/tags/%s\">#%s</a>".printf (Tootle.accounts.formal.instance, Soup.URI.encode (name, null), name);
var widget = new RichLabel (text);
widget.use_markup = true;
widget.halign = Gtk.Align.START;
@ -56,14 +58,15 @@ public class Tootle.SearchView : AbstractView {
}
private void request () {
if (entry.text == "") {
query = entry.text;
if (query == "") {
clear ();
return;
}
Tootle.window.reopen_view (this.stack_pos);
var query = Soup.URI.encode (entry.text, null);
var url = "%s/api/v1/search?q=%s".printf (Tootle.settings.instance_url, query);
var query_encoded = Soup.URI.encode (query, null);
var url = "%s/api/v1/search?q=%s".printf (Tootle.accounts.formal.instance, query_encoded);
var msg = new Soup.Message("GET", url);
Tootle.network.queue(msg, (sess, mess) => {
try{
@ -100,7 +103,6 @@ public class Tootle.SearchView : AbstractView {
}
empty_state ();
}
catch (GLib.Error e) {
warning ("Can't update feed");

View File

@ -31,7 +31,7 @@ public class Tootle.StatusView : AbstractView {
}
public Soup.Message request_context (){
var url = "%s/api/v1/statuses/%lld/context".printf (Tootle.settings.instance_url, root_status.id);
var url = "%s/api/v1/statuses/%lld/context".printf (Tootle.accounts.formal.instance, root_status.id);
var msg = new Soup.Message("GET", url);
Tootle.network.queue(msg, (sess, mess) => {
try{

View File

@ -35,14 +35,18 @@ public class Tootle.TimelineView : AbstractView {
if (timeline != this.timeline)
return;
prepend (ref status, true);
prepend (ref status);
}
public virtual bool is_status_owned (ref Status status) {
return status.is_owned ();
}
public void prepend (ref Status status, bool first = false){
public void prepend (ref Status status) {
append (ref status, true);
}
public void append (ref Status status, bool first = false){
if (empty != null)
empty.destroy ();
@ -95,12 +99,17 @@ public class Tootle.TimelineView : AbstractView {
if (page_next != null)
return page_next;
var url = "%s/api/v1/timelines/%s?limit=%i".printf (Tootle.settings.instance_url, this.timeline, this.limit);
var url = "%s/api/v1/timelines/%s?limit=%i".printf (Tootle.accounts.formal.instance, this.timeline, this.limit);
url += this.pars;
return url;
}
public virtual void request (){
if (accounts.current == null) {
empty_state ();
return;
}
var msg = new Soup.Message("GET", get_url ());
msg.finished.connect (() => empty_state ());
Tootle.network.queue(msg, (sess, mess) => {
@ -109,7 +118,7 @@ public class Tootle.TimelineView : AbstractView {
var object = node.get_object ();
if (object != null){
var status = Status.parse(object);
prepend (ref status);
append (ref status);
}
});
get_pages (mess.response_headers.get_one ("Link"));

View File

@ -5,40 +5,47 @@ public class Tootle.AccountsButton : Gtk.MenuButton{
Granite.Widgets.Avatar avatar;
Gtk.Grid grid;
Gtk.Popover menu;
AccountView default_account;
Gtk.ListBox list;
Gtk.ModelButton item_settings;
Gtk.ModelButton item_refresh;
Gtk.ModelButton item_search;
Gtk.ModelButton item_favs;
private class AccountView : Gtk.Grid{
private class AccountView : Gtk.ListBoxRow{
private Gtk.Grid grid;
public Gtk.Label display_name;
public Gtk.Label user;
public Gtk.Button logout;
construct {
margin = 6;
margin_start = 14;
public Gtk.Label instance;
public Gtk.Button button;
public int id = -1;
display_name = new Gtk.Label ("<b>Anonymous</b>");
construct {
can_default = false;
grid = new Gtk.Grid ();
grid.margin = 6;
grid.margin_start = 14;
display_name = new Gtk.Label ("");
display_name.hexpand = true;
display_name.halign = Gtk.Align.START;
display_name.use_markup = true;
user = new Gtk.Label ("@error");
user.halign = Gtk.Align.START;
logout = new Gtk.Button.from_icon_name ("pane-hide-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
logout.receives_default = false;
logout.tooltip_text = _("Log out");
logout.clicked.connect (() => Tootle.accounts.logout ());
show_all ();
instance = new Gtk.Label ("");
instance.halign = Gtk.Align.START;
button = new Gtk.Button.from_icon_name ("close-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
button.receives_default = false;
button.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT);
attach(display_name, 1, 0, 1, 1);
attach(user, 1, 1, 1, 1);
attach(logout, 2, 0, 2, 2);
grid.attach(display_name, 1, 0, 1, 1);
grid.attach(instance, 1, 1, 1, 1);
grid.attach(button, 2, 0, 2, 2);
add (grid);
show_all ();
}
public AccountView (){}
public AccountView (){
button.clicked.connect (() => Tootle.accounts.remove (id));
}
}
@ -48,7 +55,7 @@ public class Tootle.AccountsButton : Gtk.MenuButton{
return false;
});
default_account = new AccountView ();
list = new Gtk.ListBox ();
var item_separator = new Gtk.Separator (Gtk.Orientation.HORIZONTAL);
item_separator.hexpand = true;
@ -72,12 +79,12 @@ public class Tootle.AccountsButton : Gtk.MenuButton{
grid = new Gtk.Grid ();
grid.orientation = Gtk.Orientation.VERTICAL;
grid.width_request = 200;
grid.attach(default_account, 0, 1, 1, 1);
grid.attach(item_separator, 0, 2, 1, 1);
grid.attach(item_favs, 0, 3, 1, 1);
grid.attach(new Gtk.Separator (Gtk.Orientation.HORIZONTAL), 0, 4, 1, 1);
grid.attach(item_refresh, 0, 5, 1, 1);
grid.attach(item_search, 0, 6, 1, 1);
grid.attach(list, 0, 1, 1, 1);
grid.attach(item_separator, 0, 3, 1, 1);
grid.attach(item_favs, 0, 4, 1, 1);
grid.attach(new Gtk.Separator (Gtk.Orientation.HORIZONTAL), 0, 5, 1, 1);
grid.attach(item_refresh, 0, 6, 1, 1);
grid.attach(item_search, 0, 7, 1, 1);
grid.attach(item_settings, 0, 8, 1, 1);
grid.show_all ();
@ -89,17 +96,55 @@ public class Tootle.AccountsButton : Gtk.MenuButton{
add(avatar);
show_all ();
Tootle.accounts.switched.connect (account => {
if (account != null){
Tootle.image_cache.load_avatar (account.avatar, avatar, 24);
default_account.display_name.label = "<b>"+account.display_name+"</b>";
default_account.user.label = "@"+account.username;
accounts.updated.connect (accounts_updated);
accounts.switched.connect (account_switched);
list.row_activated.connect (row => {
var widget = row as AccountView;
if (widget.id == -1) {
NewAccountDialog.open ();
return;
}
if (widget.id == Tootle.settings.current_account)
return;
else
Tootle.accounts.switch_account (widget.id);
});
}
public AccountsButton(){
Object();
private void accounts_updated (GenericArray<InstanceAccount> accounts) {
list.forall (widget => widget.destroy ());
int i = -1;
accounts.foreach (account => {
i++;
var widget = new AccountView ();
widget.id = i;
widget.display_name.label = "<b>@"+account.username+"</b>";
widget.instance.label = account.instance;
list.add (widget);
});
var add_account = new AccountView ();
add_account.display_name.label = _("<b>New Account</b>");
add_account.instance.label = _("Click to add");
add_account.button.hide ();
list.add (add_account);
update_selection ();
}
private void account_switched (Account? account) {
if (account == null)
avatar.show_default (24);
else
image_cache.load_avatar (account.avatar, avatar, 24);
}
private void update_selection () {
var id = Tootle.settings.current_account;
var row = list.get_row_at_index (id);
if (row != null)
list.select_row (row);
}
public AccountsButton() {}
}

View File

@ -80,7 +80,7 @@ public class Tootle.AttachmentWidget : Gtk.EventBox {
var buffer = new Soup.Buffer.take (contents);
var multipart = new Soup.Multipart (Soup.FORM_MIME_TYPE_MULTIPART);
multipart.append_form_file ("file", mime.replace ("/", "."), mime, buffer);
var url = "%s/api/v1/media".printf (Tootle.settings.instance_url);
var url = "%s/api/v1/media".printf (Tootle.accounts.formal.instance);
var msg = Soup.Form.request_new_from_multipart (url, multipart);
Tootle.network.queue(msg, (sess, mess) => {

View File

@ -1,82 +0,0 @@
using Gtk;
public class Tootle.HeaderBar : Gtk.HeaderBar{
public Granite.Widgets.ModeButton button_mode;
AccountsButton button_accounts;
Spinner spinner;
Button button_toot;
Button button_back;
private int last_tab = 0;
construct {
spinner = new Spinner ();
spinner.active = true;
button_accounts = new AccountsButton ();
button_back = new Button ();
button_back.label = _("Back");
button_back.get_style_context ().add_class (Granite.STYLE_CLASS_BACK_BUTTON);
button_back.clicked.connect (() => {
var primary_stack = Tootle.window.primary_stack;
var i = int.parse (primary_stack.get_visible_child_name ());
primary_stack.set_visible_child_name ((i-1).to_string ());
var child = primary_stack.get_child_by_name (i.to_string ());
child.destroy ();
var is_root = primary_stack.get_visible_child_name () == "0";
update (is_root);
});
button_toot = new Button ();
button_toot.tooltip_text = "Toot";
button_toot.image = new Gtk.Image.from_icon_name ("edit-symbolic", Gtk.IconSize.LARGE_TOOLBAR);
button_toot.clicked.connect (() => PostDialog.open ());
button_mode = new Granite.Widgets.ModeButton ();
button_mode.get_style_context ().add_class ("mode");
button_mode.mode_changed.connect(widget => {
last_tab = button_mode.selected;
Tootle.window.secondary_stack.set_visible_child_name(widget.tooltip_text);
});
button_mode.show ();
Tootle.network.started.connect (() => spinner.show ());
Tootle.network.finished.connect (() => spinner.hide ());
pack_start (button_back);
pack_start (button_toot);
pack_end (button_accounts);
pack_end (spinner);
}
public HeaderBar () {
custom_title = button_mode;
show_close_button = true;
show ();
button_mode.valign = Gtk.Align.FILL;
}
public void update (bool primary_mode, bool hide_all = false){
button_mode.set_active (last_tab);
if (hide_all){
//button_mode.opacity = 0;
//button_mode.sensitive = false;
button_mode.hide ();
button_toot.hide ();
button_back.hide ();
button_accounts.hide ();
return;
}
//button_mode.opacity = primary_mode ? 1 : 0;
//button_mode.sensitive = primary_mode ? true : false;
button_mode.set_visible (primary_mode);
button_toot.set_visible (primary_mode);
button_back.set_visible (!primary_mode);
button_accounts.set_visible (true);
}
}