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

Rewrite attachment view

This commit is contained in:
bleakgrey 2019-03-07 19:16:52 +03:00
parent 09119b4584
commit 3305fac395
15 changed files with 598 additions and 483 deletions

View File

@ -20,6 +20,7 @@ executable(
asresources,
'src/Application.vala',
'src/Desktop.vala',
'src/Drawing.vala',
'src/Html.vala',
'src/MainWindow.vala',
'src/Settings.vala',
@ -45,8 +46,8 @@ executable(
'src/Widgets/StatusWidget.vala',
'src/Widgets/AccountWidget.vala',
'src/Widgets/NotificationWidget.vala',
'src/Widgets/AttachmentWidget.vala',
'src/Widgets/AttachmentBox.vala',
'src/Widgets/ImageAttachment.vala',
'src/Widgets/AttachmentGrid.vala',
'src/Dialogs/NewAccountDialog.vala',
'src/Dialogs/PostDialog.vala',
'src/Dialogs/SettingsDialog.vala',

View File

@ -1,4 +1,4 @@
public class Tootle.Attachment{
public class Tootle.Attachment {
public int64 id;
public string type;
@ -6,24 +6,24 @@ public class Tootle.Attachment{
public string preview_url;
public string? description;
public Attachment(int64 _id){
public Attachment(int64 _id) {
id = _id;
}
public static Attachment parse (Json.Object obj){
public static Attachment parse (Json.Object obj) {
var id = int64.parse (obj.get_string_member ("id"));
var attachment = new Attachment (id);
attachment.type = obj.get_string_member ("type");
attachment.preview_url = obj.get_string_member ("preview_url");
attachment.url = obj.get_string_member ("url");
if (obj.has_member ("description"))
attachment.description = obj.get_string_member ("description");
return attachment;
}
public Json.Node? serialize () {
var builder = new Json.Builder ();
builder.begin_object ();
@ -35,7 +35,7 @@ public class Tootle.Attachment{
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);

View File

@ -2,9 +2,9 @@ using Gtk;
using Tootle;
public class Tootle.PostDialog : Gtk.Dialog {
private static PostDialog dialog;
protected TextView text;
private ScrolledWindow scroll;
private Label counter;
@ -13,10 +13,10 @@ public class Tootle.PostDialog : Gtk.Dialog {
private Button attach;
private Button cancel;
private Button publish;
protected AttachmentBox attachments;
protected Widgets.AttachmentGrid attachments;
private Revealer spoiler_revealer;
private Entry spoiler_text;
protected Status? replying_to;
protected Status? redrafting;
protected StatusVisibility visibility_opt = StatusVisibility.PUBLIC;
@ -31,23 +31,23 @@ public class Tootle.PostDialog : Gtk.Dialog {
char_limit = settings.char_limit;
replying_to = _replying_to;
redrafting = _redrafting;
if (replying_to != null)
visibility_opt = replying_to.visibility;
if (redrafting != null)
visibility_opt = redrafting.visibility;
var actions = get_action_area ().get_parent () as Gtk.Box;
var content = get_content_area ();
get_action_area ().hexpand = false;
visibility = get_visibility_btn ();
visibility.tooltip_text = _("Post Visibility");
visibility.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT);
visibility.get_style_context ().remove_class ("image-button");
visibility.can_default = false;
(visibility as Widget).set_focus_on_click (false);
attach = new Button.from_icon_name ("mail-attachment-symbolic");
attach.tooltip_text = _("Add Media");
attach.valign = Gtk.Align.CENTER;
@ -56,7 +56,7 @@ public class Tootle.PostDialog : Gtk.Dialog {
attach.can_default = false;
(attach as Widget).set_focus_on_click (false);
attach.clicked.connect (() => attachments.select ());
spoiler = new ImageToggleButton ("image-red-eye-symbolic");
spoiler.tooltip_text = _("Spoiler Warning");
spoiler.set_action ();
@ -64,10 +64,10 @@ public class Tootle.PostDialog : Gtk.Dialog {
spoiler_revealer.reveal_child = spoiler.active;
validate ();
});
cancel = add_button (_("Cancel"), 5) as Button;
cancel.clicked.connect(() => destroy ());
if (redrafting != null) {
publish = add_button (_("Redraft"), 5) as Button;
publish.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION);
@ -78,23 +78,23 @@ public class Tootle.PostDialog : Gtk.Dialog {
publish.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION);
publish.clicked.connect (publish_post);
}
spoiler_text = new Gtk.Entry ();
spoiler_text.margin_start = 6;
spoiler_text.margin_end = 6;
spoiler_text.placeholder_text = _("Write your warning here");
spoiler_text.changed.connect (validate);
spoiler_revealer = new Gtk.Revealer ();
spoiler_revealer.add (spoiler_text);
text = new TextView ();
text.get_style_context ().add_class ("toot-text");
text.wrap_mode = Gtk.WrapMode.WORD;
text.accepts_tab = false;
text.vexpand = true;
text.buffer.changed.connect (validate);
scroll = new ScrolledWindow (null, null);
scroll.hscrollbar_policy = Gtk.PolicyType.NEVER;
scroll.min_content_height = 120;
@ -104,10 +104,10 @@ public class Tootle.PostDialog : Gtk.Dialog {
scroll.margin_end = 6;
scroll.add (text);
scroll.show_all ();
attachments = new AttachmentBox (true);
attachments = new Widgets.AttachmentGrid (true);
counter = new Label ("");
actions.pack_start (counter, false, false, 6);
actions.pack_end (spoiler, false, false, 6);
actions.pack_end (visibility, false, false, 0);
@ -116,7 +116,7 @@ public class Tootle.PostDialog : Gtk.Dialog {
content.pack_start (scroll, false, false, 6);
content.pack_start (attachments, false, false, 6);
content.set_size_request (350, 120);
if (replying_to != null) {
spoiler.active = replying_to.sensitive;
var status_spoiler_text = replying_to.spoiler_text != null ? replying_to.spoiler_text : "";
@ -127,15 +127,15 @@ public class Tootle.PostDialog : Gtk.Dialog {
var status_spoiler_text = redrafting.spoiler_text != null ? redrafting.spoiler_text : "";
spoiler_text.set_text (status_spoiler_text);
}
destroy.connect (() => dialog = null);
show_all ();
attachments.hide ();
text.grab_focus ();
validate ();
}
private Gtk.MenuButton get_visibility_btn () {
var button = new Gtk.MenuButton ();
var menu = new Gtk.Popover (null);
@ -144,13 +144,13 @@ public class Tootle.PostDialog : Gtk.Dialog {
menu.add (box);
button.direction = Gtk.ArrowType.DOWN;
button.image = new Gtk.Image.from_icon_name (visibility_opt.get_icon (), Gtk.IconSize.BUTTON);
Gtk.RadioButton? first = null;
foreach (StatusVisibility opt in StatusVisibility.get_all ()){
var item = new Gtk.RadioButton.with_label_from_widget (first, opt.get_desc ());
if (first == null)
first = item;
item.toggled.connect (() => {
visibility_opt = opt;
(button.image as Gtk.Image).icon_name = visibility_opt.get_icon ();
@ -158,7 +158,7 @@ public class Tootle.PostDialog : Gtk.Dialog {
item.active = visibility_opt == opt;
box.pack_start (item, false, false, 0);
}
box.show_all ();
button.use_popover = true;
button.popover = menu;
@ -166,62 +166,62 @@ public class Tootle.PostDialog : Gtk.Dialog {
button.show ();
return button;
}
private void validate () {
var remain = char_limit - text.buffer.text.length;
if (spoiler.active)
remain -= spoiler_text.buffer.text.length;
counter.label = remain.to_string ();
publish.sensitive = remain >= 0;
publish.sensitive = remain >= 0;
}
public static void open (string? text = null, Status? reply_to = null) {
if (dialog == null){
dialog = new PostDialog (reply_to);
if (text != null)
dialog.text.buffer.text = text;
}
else if (text != null)
dialog.text.buffer.text += text;
}
public static void reply (Status status) {
if (dialog != null)
return;
open (null, status);
dialog.text.buffer.text = status.get_reply_mentions ();
}
public static void redraft (Status status) {
if (dialog != null)
return;
dialog = new PostDialog (null, status);
if (status.attachments != null) {
foreach (Attachment attachment in status.attachments)
dialog.attachments.append (attachment);
}
var content = Html.simplify (status.content);
content = Html.remove_tags (content);
content = RichLabel.restore_entities (content);
dialog.text.buffer.text = content;
}
private void publish_post () {
var pars = "?status=%s&visibility=%s".printf (Html.uri_encode (text.buffer.text), visibility_opt.to_string ());
pars += attachments.get_uri_array ();
if (replying_to != null)
pars += "&in_reply_to_id=%s".printf (replying_to.id.to_string ());
if (spoiler.active) {
pars += "&sensitive=true";
pars += "&spoiler_text=" + Html.uri_encode (spoiler_text.buffer.text);
}
var url = "%s/api/v1/statuses%s".printf (accounts.formal.instance, pars);
var msg = new Soup.Message ("POST", url);
network.queue (msg, (sess, mess) => {
@ -237,7 +237,7 @@ public class Tootle.PostDialog : Gtk.Dialog {
}
});
}
private void redraft_post () {
redrafting.poof (false).finished.connect (publish_post);
}

35
src/Drawing.vala Normal file
View File

@ -0,0 +1,35 @@
using Gdk;
using GLib;
public class Tootle.Drawing {
public static void draw_rounded_rect (Cairo.Context ctx, double x, double y, double w, double h, double r) {
double degr = Math.PI / 180.0;
ctx.new_sub_path ();
ctx.arc (x + w - r, y + r, r, -90 * degr, 0 * degr);
ctx.arc (x + w - r, y + h - r, r, 0 * degr, 90 * degr);
ctx.arc (x + r, y + h - r, r, 90 * degr, 180 * degr);
ctx.arc (x + r, y + r, r, 180 * degr, 270 * degr);
ctx.close_path ();
}
public static Pixbuf make_pixbuf_thumbnail (Pixbuf pixbuf, int view_w, int view_h, bool fill_parent = false) {
// Don't resize if parent view is bigger than actual image
if (view_w >= pixbuf.width && view_h >= pixbuf.height)
return pixbuf;
//Otherwise fit the image into the parent view
var resized_w = view_w;
var resized_h = view_h;
//resized_w = (pixbuf.width * view_h) / pixbuf.height;
//resized_h = (pixbuf.height * view_w) / pixbuf.width;
if (fill_parent)
resized_h = (pixbuf.height * view_w) / pixbuf.width;
else
resized_w = (pixbuf.width * view_h) / pixbuf.height;
return pixbuf.scale_simple (resized_w, resized_h, InterpType.BILINEAR);
}
}

View File

@ -1,20 +1,20 @@
using Gtk;
public class Tootle.MainWindow: Gtk.Window {
private Overlay overlay;
private Granite.Widgets.Toast toast;
private Grid grid;
private Stack primary_stack;
private Stack secondary_stack;
public HeaderBar header;
public Granite.Widgets.ModeButton button_mode;
private AccountsButton button_accounts;
private Spinner spinner;
private Button button_toot;
private Button button_back;
public HomeView home = new HomeView ();
public NotificationsView notifications = new NotificationsView ();
public LocalView local = new LocalView ();
@ -24,7 +24,7 @@ public class Tootle.MainWindow: Gtk.Window {
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);
settings.changed.connect (update_theme);
update_theme ();
@ -37,18 +37,18 @@ public class Tootle.MainWindow: Gtk.Window {
primary_stack.add_named (secondary_stack, "0");
primary_stack.hexpand = true;
primary_stack.vexpand = true;
spinner = new Spinner ();
spinner.active = true;
button_accounts = new AccountsButton ();
button_back = new Button ();
button_back.valign = Align.CENTER;
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.valign = Align.CENTER;
button_toot.tooltip_text = _("Toot");
@ -61,7 +61,7 @@ public class Tootle.MainWindow: Gtk.Window {
button_mode.valign = Align.FILL;
button_mode.mode_changed.connect (on_mode_changed);
button_mode.show ();
header = new HeaderBar ();
header.get_style_context ().add_class ("compact");
header.show_close_button = true;
@ -72,16 +72,16 @@ public class Tootle.MainWindow: Gtk.Window {
header.pack_end (button_accounts);
header.pack_end (spinner);
header.show_all ();
grid = new Grid ();
grid.attach (primary_stack, 0, 0, 1, 1);
add_header_view (home);
add_header_view (notifications);
add_header_view (local);
add_header_view (federated);
button_mode.set_active (0);
toast = new Granite.Widgets.Toast ("");
overlay = new Overlay ();
overlay.add_overlay (grid);
@ -90,7 +90,7 @@ public class Tootle.MainWindow: Gtk.Window {
add (overlay);
show_all ();
}
public MainWindow (Gtk.Application _app) {
application = _app;
icon_name = "com.github.bleakgrey.tootle";
@ -98,7 +98,7 @@ public class Tootle.MainWindow: Gtk.Window {
window_position = WindowPosition.CENTER;
set_titlebar (header);
update_header ();
app.toast.connect (on_toast);
network.started.connect (() => spinner.show ());
network.finished.connect (() => spinner.hide ());
@ -111,22 +111,22 @@ public class Tootle.MainWindow: Gtk.Window {
return false;
});
}
private void add_header_view (AbstractView view) {
var img = new Image.from_icon_name (view.get_icon (), IconSize.LARGE_TOOLBAR);
img.tooltip_text = view.get_name ();
button_mode.append (img);
view.image = img;
secondary_stack.add_named (view, view.get_name ());
if (view is NotificationsView)
img.pixel_size = 20; // For some reason Notifications icon is too small without this
}
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++;
@ -136,18 +136,18 @@ public class Tootle.MainWindow: Gtk.Window {
primary_stack.set_visible_child_name (i.to_string ());
update_header ();
}
public void back () {
var i = get_visible_id ();
if (i == 0)
return;
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) {
@ -155,7 +155,7 @@ public class Tootle.MainWindow: Gtk.Window {
i = get_visible_id ();
}
}
public override bool delete_event (Gdk.EventAny event) {
this.destroy.connect (() => {
if (!settings.always_online || accounts.is_empty ())
@ -164,11 +164,11 @@ public class Tootle.MainWindow: Gtk.Window {
});
return false;
}
public void switch_timeline (int32 timeline_no) {
button_mode.set_active (timeline_no);
}
private void update_theme () {
var provider = new Gtk.CssProvider ();
var is_dark = settings.dark_theme;
@ -177,7 +177,7 @@ public class Tootle.MainWindow: Gtk.Window {
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.sensitive = primary_mode;
@ -195,9 +195,9 @@ public class Tootle.MainWindow: Gtk.Window {
private void on_mode_changed (Widget widget) {
var visible = secondary_stack.get_visible_child () as AbstractView;
visible.current = false;
secondary_stack.set_visible_child_name (widget.tooltip_text);
visible = secondary_stack.get_visible_child () as AbstractView;
visible.current = true;
visible.on_set_current ();

View File

@ -7,10 +7,10 @@ public class Tootle.Network : GLib.Object {
public signal void started ();
public signal void finished ();
public signal void notification (Notification notification);
public signal void status_removed (int64 id);
private int requests_processing = 0;
private Soup.Session session;
@ -19,31 +19,31 @@ public class Tootle.Network : GLib.Object {
session.ssl_strict = true;
session.ssl_use_system_ca_file = true;
session.timeout = 20;
session.max_conns = 15;
session.max_conns = 20;
session.request_unqueued.connect (msg => {
requests_processing--;
if(requests_processing <= 0)
finished ();
});
// Soup.Logger logger = new Soup.Logger (Soup.LoggerLogLevel.BODY, -1);
// session.add_feature (logger);
}
public Network () {}
public async WebsocketConnection stream (Soup.Message msg) throws GLib.Error {
return yield session.websocket_connect_async (msg, null, null, null);
}
public Soup.Message queue (Soup.Message msg, owned Soup.SessionCallback? cb = null) {
requests_processing++;
started ();
var formal = accounts.formal;
if(formal != null)
msg.request_headers.append ("Authorization", "Bearer " + formal.token);
session.queue_message (msg, (sess, mess) => {
switch (mess.tls_errors){
case GLib.TlsCertificateFlags.UNKNOWN_CA:
@ -60,16 +60,16 @@ public class Tootle.Network : GLib.Object {
default:
break;
}
if (mess.status_code != Soup.Status.OK) {
var phrase = Soup.Status.get_phrase (mess.status_code);
app.toast (_("Error: %s").printf(phrase));
return;
}
if (cb != null)
cb (sess, mess);
msg.request_body.free ();
msg.response_body.free ();
msg.request_headers.free ();
@ -77,11 +77,11 @@ public class Tootle.Network : GLib.Object {
});
return msg;
}
public void queue_custom (Soup.Message msg, owned Soup.SessionCallback? cb = null) {
requests_processing++;
started ();
msg.finished.connect_after (() => {
msg.request_body.free ();
msg.response_body.free ();
@ -90,40 +90,40 @@ public class Tootle.Network : GLib.Object {
});
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);
// debug ("Object: %s", (string) msg.response_body.data);
var parser = new Json.Parser ();
parser.load_from_data ((string) msg.response_body.flatten ().data, -1);
return parser.get_root ().get_object ();
}
public Json.Array parse_array (Soup.Message msg) throws GLib.Error {
// debug ("Status Code: %u", msg.status_code);
// debug ("Message length: %lld", msg.response_body.length);
// debug ("Array: %s", (string) msg.response_body.data);
var parser = new Json.Parser ();
parser.load_from_data ((string) msg.response_body.flatten ().data, -1);
return parser.get_root ().get_array ();
}
public void load_avatar (string url, Granite.Widgets.Avatar avatar, int size){
if (settings.cache) {
image_cache.load_avatar (url, avatar, size);
return;
}
var msg = new Soup.Message("GET", url);
msg.finished.connect(() => {
if (msg.status_code != Soup.Status.OK) {
avatar.show_default (size);
return;
}
var data = msg.response_body.data;
var stream = new MemoryInputStream.from_data (data);
var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true);
@ -131,20 +131,47 @@ public class Tootle.Network : GLib.Object {
});
network.queue_custom (msg);
}
public delegate void PixbufCallback (Gdk.Pixbuf pixbuf);
public void load_pixbuf (string url, PixbufCallback cb) {
var msg = new Soup.Message("GET", url);
ulong signal_id = 0;
signal_id = msg.finished.connect (() => {
Gdk.Pixbuf? pixbuf = null;
try {
var data = msg.response_body.flatten().data;
var stream = new MemoryInputStream.from_data (data);
pixbuf = new Gdk.Pixbuf.from_stream (stream);
}
catch (Error e) {
warning ("Can't get image: %s".printf (url));
warning ("Reason: " + e.message);
}
finally {
if (msg.status_code != Soup.Status.OK)
warning ("Response code %s: %s".printf (msg.status_code.to_string (), url));
}
cb (pixbuf);
msg.disconnect (signal_id);
});
network.queue_custom (msg);
}
public void load_image (string url, Gtk.Image image) {
if (settings.cache) {
image_cache.load_image (url, image);
return;
}
// if (settings.cache) {
// image_cache.load_image (url, image);
// return;
// }
var msg = new Soup.Message("GET", url);
msg.finished.connect(() => {
if (msg.status_code != Soup.Status.OK) {
image.set_from_icon_name ("image-missing", Gtk.IconSize.LARGE_TOOLBAR);
return;
}
var data = msg.response_body.data;
var stream = new MemoryInputStream.from_data (data);
var pixbuf = new Gdk.Pixbuf.from_stream (stream);
@ -152,13 +179,13 @@ public class Tootle.Network : GLib.Object {
});
network.queue_custom (msg);
}
public void load_scaled_image (string url, Gtk.Image image, int size) {
if (settings.cache) {
image_cache.load_scaled_image (url, image, size);
return;
}
// if (settings.cache) {
// image_cache.load_scaled_image (url, image, size);
// return;
// }
var msg = new Soup.Message("GET", url);
msg.finished.connect(() => {
if (msg.status_code != Soup.Status.OK) {
@ -173,5 +200,5 @@ public class Tootle.Network : GLib.Object {
});
network.queue_custom (msg);
}
}

View File

@ -2,10 +2,10 @@ using Gtk;
using Granite;
public class Tootle.AccountView : TimelineView {
const int AVATAR_SIZE = 128;
protected Account account;
protected Grid header_image;
protected Box header_info;
protected Granite.Widgets.Avatar avatar;
@ -16,7 +16,7 @@ public class Tootle.AccountView : TimelineView {
protected Grid counters;
protected Box actions;
protected Button button_follow;
protected Gtk.Menu menu;
protected Gtk.MenuItem menu_edit;
protected Gtk.MenuItem menu_mention;
@ -24,8 +24,8 @@ public class Tootle.AccountView : TimelineView {
protected Gtk.MenuItem menu_block;
protected Gtk.MenuItem menu_report;
protected Gtk.MenuButton button_menu;
construct {
header = new Grid ();
header_info = new Box (Orientation.VERTICAL, 0);
@ -36,26 +36,26 @@ public class Tootle.AccountView : TimelineView {
actions.vexpand = false;
actions.valign = Align.START;
actions.margin = 12;
relationship = new Label ("");
relationship.get_style_context ().add_class ("relationship");
relationship.halign = Align.START;
relationship.valign = Align.START;
relationship.margin = 12;
header.attach (relationship, 0, 0, 1, 1);
avatar = new Granite.Widgets.Avatar.with_default_icon (AVATAR_SIZE);
avatar.hexpand = true;
avatar.margin_bottom = 6;
header_info.pack_start (avatar, false, false, 0);
display_name = new RichLabel ("");
display_name.get_style_context ().add_class (Granite.STYLE_CLASS_H2_LABEL);
header_info.pack_start (display_name, false, false, 0);
username = new Gtk.Label ("");
header_info.pack_start (username, false, false, 0);
note = new RichLabel ("");
note.set_line_wrap (true);
note.selectable = true;
@ -65,16 +65,16 @@ public class Tootle.AccountView : TimelineView {
header_info.pack_start (note, false, false, 0);
header_info.show_all ();
header.attach (header_info, 0, 0, 1, 1);
counters = new Grid ();
counters.column_homogeneous = true;
counters.get_style_context ().add_class ("header-counters");
header.attach (counters, 0, 1, 1, 1);
header_image = new Grid ();
header_image.get_style_context ().add_class ("header");
header.attach (header_image, 0, 0, 2, 2);
menu = new Gtk.Menu ();
menu_edit = new Gtk.MenuItem.with_label (_("Edit Profile"));
menu_mention = new Gtk.MenuItem.with_label (_("Mention"));
@ -88,7 +88,7 @@ public class Tootle.AccountView : TimelineView {
//menu.add (menu_report); //TODO: Report users
//menu.add (menu_edit); //TODO: Edit profile
menu.show_all ();
button_follow = add_counter ("contact-new-symbolic");
button_menu = new Gtk.MenuButton ();
button_menu.image = new Image.from_icon_name ("view-more-symbolic", IconSize.LARGE_TOOLBAR);
@ -103,15 +103,15 @@ public class Tootle.AccountView : TimelineView {
button_menu.hide ();
button_follow.hide ();
header.attach (actions, 0, 0, 2, 2);
view.pack_start (header, false, false, 0);
}
public AccountView (Account acc) {
base ("");
account = acc;
account.updated.connect(rebind);
add_counter (_("Toots"), 1, account.statuses_count);
add_counter (_("Follows"), 2, account.following_count).clicked.connect (() => {
var view = new FollowingView (account);
@ -121,34 +121,35 @@ public class Tootle.AccountView : TimelineView {
var view = new FollowersView (account);
window.open_view (view);
});
show_all ();
var stylesheet = ".header{background-image: url(\"%s\")}".printf (account.header);
var css_provider = Granite.Widgets.Utils.get_css_provider (stylesheet);
header_image.get_style_context ().add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
//TODO: Has this thing always been synchronous???
//var stylesheet = ".header{background-image: url(\"%s\")}".printf (account.header);
//var css_provider = Granite.Widgets.Utils.get_css_provider (stylesheet);
//header_image.get_style_context ().add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
menu_mention.activate.connect (() => PostDialog.open ("@%s ".printf (account.acct)));
menu_mute.activate.connect (() => account.set_muted (!account.rs.muting));
menu_block.activate.connect (() => account.set_blocked (!account.rs.blocking));
button_follow.clicked.connect (() => account.set_following (!account.rs.following));
rebind ();
account.get_relationship ();
request ();
}
public void rebind (){
display_name.set_label ("<b>%s</b>".printf (account.display_name));
username.label = "@" + account.acct;
note.set_label (Html.simplify (account.note));
button_follow.visible = !account.is_self ();
network.load_avatar (account.avatar, avatar, 128);
menu_edit.visible = account.is_self ();
if (account.rs != null && !account.is_self ()) {
button_follow.show ();
if (account.rs.following) {
@ -160,13 +161,13 @@ public class Tootle.AccountView : TimelineView {
(button_follow.get_image () as Image).icon_name = "contact-new-symbolic";
}
}
if (account.rs != null){
button_menu.show ();
menu_block.label = account.rs.blocking ? _("Unblock") : _("Block");
menu_mute.label = account.rs.muting ? _("Unmute") : _("Mute");
menu_report.visible = menu_mute.visible = menu_block.visible = !account.is_self ();
var rs_label = get_relationship_label ();
if (rs_label != null) {
relationship.label = rs_label;
@ -178,11 +179,11 @@ public class Tootle.AccountView : TimelineView {
else
relationship.hide ();
}
public override bool is_status_owned (Status status) {
return status.is_owned ();
}
private Gtk.Button add_counter (string name, int? i = null, int64? val = null) {
Button btn;
if (val != null){
@ -195,34 +196,34 @@ public class Tootle.AccountView : TimelineView {
}
else
btn = new Button.from_icon_name (name, IconSize.LARGE_TOOLBAR);
btn.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT);
(btn as Widget).set_focus_on_click (false);
btn.can_default = false;
btn.can_focus = false;
if (i != null)
counters.attach (btn, i, 1, 1, 1);
return btn;
}
public override bool is_empty () {
return view.get_children ().length () <= 2;
}
public override string get_url () {
if (page_next != null)
return page_next;
var url = "%s/api/v1/accounts/%lld/statuses?limit=%i".printf (accounts.formal.instance, account.id, this.limit);
return url;
}
public override void request () {
if(account != null)
base.request ();
}
private string? get_relationship_label () {
if (account.rs.requested)
return _("Sent follow request");
@ -235,7 +236,7 @@ public class Tootle.AccountView : TimelineView {
else
return null;
}
public static void open_from_id (int64 id){
var url = "%s/api/v1/accounts/%lld".printf (accounts.formal.instance, id);
var msg = new Soup.Message ("GET", url);
@ -252,7 +253,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 (accounts.formal.instance, name);
var msg = new Soup.Message("GET", url);
@ -273,5 +274,5 @@ public class Tootle.AccountView : TimelineView {
}
});
}
}

View File

@ -3,15 +3,15 @@ public class Tootle.HomeView : TimelineView {
public HomeView () {
base ("home");
}
public override string get_icon () {
return "user-home-symbolic";
}
public override string get_name () {
return _("Home");
}
public override Soup.Message? get_stream () {
return accounts.formal.get_stream ();
}

View File

@ -2,56 +2,56 @@ using Gtk;
using Gdk;
public class Tootle.TimelineView : AbstractView {
protected string timeline;
protected string pars;
protected int limit = 25;
protected bool is_last_page = false;
protected string? page_next;
protected string? page_prev;
protected Notificator? notificator;
public TimelineView (string timeline, string pars = "") {
base ();
this.timeline = timeline;
this.pars = pars;
accounts.switched.connect (on_account_changed);
app.refresh.connect (on_refresh);
destroy.connect (() => {
if (notificator != null)
notificator.close ();
});
setup_notificator ();
request ();
}
public override string get_icon () {
return "user-home-symbolic";
}
public override string get_name () {
return _("Home");
}
public virtual void on_status_added (Status status) {
prepend (status);
}
public virtual bool is_status_owned (Status status) {
return false;
}
public void prepend (Status status) {
append (status, true);
}
public void append (Status status, bool first = false){
if (empty != null)
empty.destroy ();
var separator = new Separator (Orientation.HORIZONTAL);
separator.show ();
@ -62,26 +62,26 @@ public class Tootle.TimelineView : AbstractView {
widget.avatar.button_press_event.connect (widget.open_account);
view.pack_start (separator, false, false, 0);
view.pack_start (widget, false, false, 0);
if (first || status.pinned) {
var new_index = header == null ? 1 : 0;
view.reorder_child (separator, new_index);
view.reorder_child (widget, new_index);
}
}
public override void clear () {
this.page_prev = null;
this.page_next = null;
this.is_last_page = false;
base.clear ();
}
public void get_pages (string? header) {
page_next = page_prev = null;
if (header == null)
return;
var pages = header.split (",");
foreach (var page in pages) {
var sanitized = page
@ -94,25 +94,25 @@ public class Tootle.TimelineView : AbstractView {
else
page_next = sanitized;
}
is_last_page = page_prev != null & page_next == null;
}
public virtual string get_url () {
if (page_next != null)
return page_next;
var url = "%s/api/v1/timelines/%s?limit=%i".printf (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 ());
network.queue(msg, (sess, mess) => {
@ -132,20 +132,20 @@ public class Tootle.TimelineView : AbstractView {
}
});
}
public virtual void on_refresh (){
clear ();
request ();
}
public virtual Soup.Message? get_stream (){
return null;
}
public virtual void on_account_changed (Account? account){
if(account == null)
return;
var stream = get_stream ();
if (notificator != null && stream != null) {
var old_url = notificator.get_url ();
@ -155,18 +155,18 @@ public class Tootle.TimelineView : AbstractView {
setup_notificator ();
}
}
on_refresh ();
}
protected void setup_notificator () {
if (notificator != null)
notificator.close ();
var stream = get_stream ();
if (stream == null)
return;
notificator = new Notificator (stream);
notificator.status_added.connect ((status) => {
if (can_stream ())
@ -174,19 +174,19 @@ public class Tootle.TimelineView : AbstractView {
});
notificator.start ();
}
protected virtual bool is_public () {
return false;
}
protected virtual bool can_stream () {
var allowed_public = true;
if (is_public ())
allowed_public = settings.live_updates_public;
return settings.live_updates && allowed_public;
}
protected override void on_bottom_reached () {
if (is_last_page) {
debug ("Last page reached");

View File

@ -1,72 +0,0 @@
using Gtk;
using GLib;
public class Tootle.AttachmentBox : Gtk.ScrolledWindow {
private Gtk.Box box;
private bool edit_mode;
construct {
box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
box.hexpand = true;
add (box);
show_all ();
}
public AttachmentBox (bool edit = false) {
Object ();
edit_mode = edit;
vscrollbar_policy = Gtk.PolicyType.NEVER;
}
public void clear () {
box.forall (widget => widget.destroy ());
}
public void append (Attachment attachment) {
show ();
var widget = new AttachmentWidget (attachment, edit_mode);
box.add (widget);
}
public void select () {
var filter = new Gtk.FileFilter ();
filter.add_mime_type ("image/jpeg");
filter.add_mime_type ("image/png");
filter.add_mime_type ("image/gif");
filter.add_mime_type ("video/webm");
filter.add_mime_type ("video/mp4");
var chooser = new Gtk.FileChooserDialog (
_("Select media files to add"),
null,
Gtk.FileChooserAction.OPEN,
_("_Cancel"),
Gtk.ResponseType.CANCEL,
_("_Open"),
Gtk.ResponseType.ACCEPT);
chooser.select_multiple = true;
chooser.set_filter (filter);
if (chooser.run () == Gtk.ResponseType.ACCEPT) {
show ();
foreach (unowned string uri in chooser.get_uris ()) {
var widget = new AttachmentWidget.upload (uri);
box.pack_start (widget, false, false, 6);
}
}
chooser.close ();
}
public string get_uri_array () {
var str = "";
box.get_children ().@foreach (widget => {
var w = (AttachmentWidget) widget;
if (w.attachment != null)
str += "&media_ids[]=%lld".printf (w.attachment.id);
});
return str;
}
}

View File

@ -0,0 +1,91 @@
using Gtk;
using GLib;
public class Tootle.Widgets.AttachmentGrid : Grid {
private int counter = 0;
private bool allow_editing;
construct {
hexpand = true;
}
public AttachmentGrid (bool edit = false) {
allow_editing = edit;
}
public void append (Attachment attachment) {
var widget = new ImageAttachment (attachment);
attach_widget (widget);
}
public void append_widget (ImageAttachment widget) {
attach_widget (widget);
}
private void attach_widget (ImageAttachment widget) {
attach (widget, counter++, 1);
column_spacing = row_spacing = 12;
show_all ();
}
public void pack (Attachment[] attachments) {
clear ();
var len = attachments.length;
if (len == 1) {
var widget = new ImageAttachment (attachments[0]);
attach_widget (widget);
widget.fill_parent ();
}
else {
foreach (Attachment attachment in attachments) {
append (attachment);
}
}
}
private void clear () {
forall (widget => widget.destroy ());
}
public void select () {
var filter = new Gtk.FileFilter ();
filter.add_mime_type ("image/jpeg");
filter.add_mime_type ("image/png");
filter.add_mime_type ("image/gif");
filter.add_mime_type ("video/webm");
filter.add_mime_type ("video/mp4");
var chooser = new Gtk.FileChooserDialog (
_("Select media files to add"),
null,
Gtk.FileChooserAction.OPEN,
_("_Cancel"),
Gtk.ResponseType.CANCEL,
_("_Open"),
Gtk.ResponseType.ACCEPT);
chooser.select_multiple = true;
chooser.set_filter (filter);
if (chooser.run () == Gtk.ResponseType.ACCEPT) {
show ();
foreach (unowned string uri in chooser.get_uris ()) {
var widget = new ImageAttachment.upload (uri);
append_widget (widget);
}
}
chooser.close ();
}
public string get_uri_array () {
var str = "";
get_children ().@foreach (w => {
var widget = (ImageAttachment) w;
if (widget.attachment != null)
str += "&media_ids[]=%lld".printf (widget.attachment.id);
});
return str;
}
}

View File

@ -1,148 +0,0 @@
using Gtk;
using Gdk;
public class Tootle.AttachmentWidget : Gtk.EventBox {
public Attachment? attachment;
private bool editable;
private const int PREVIEW_SIZE = 350;
private const int SMALL_SIZE = 64;
public Gtk.Label label;
private Gtk.Grid grid;
private Gtk.Image? image;
construct {
set_size_request (SMALL_SIZE, SMALL_SIZE);
hexpand = false;
grid = new Gtk.Grid ();
get_style_context ().add_class ("attachment");
label = new Gtk.Label ("");
label.wrap = true;
label.vexpand = true;
label.margin_start = label.margin_end = 8;
grid.attach (label, 0, 0);
add (grid);
grid.show ();
label.hide ();
destroy.connect (() => {
if (image != null)
image.clear ();
});
}
public AttachmentWidget (Attachment att, bool _editable = false) {
attachment = att;
editable = _editable;
rebind ();
}
public int get_size (int size) {
return size * get_style_context ().get_scale ();
}
public void rebind () {
var type = attachment.type;
switch (type){
case "image":
image = new Gtk.Image ();
image.vexpand = true;
image.hexpand = true;
image.valign = Gtk.Align.CENTER;
image.halign = Gtk.Align.CENTER;
image.margin = 3;
image.set_tooltip_text (attachment.description);
image.show ();
var size = editable ? SMALL_SIZE : PREVIEW_SIZE;
network.load_scaled_image (attachment.preview_url, image, get_size (size));
grid.attach (image, 0, 0);
label.hide ();
break;
default:
label.label = _("Click to open %s media").printf (type);
label.show ();
break;
}
show ();
button_press_event.connect (on_clicked);
}
public AttachmentWidget.upload (string uri) {
try {
GLib.File file = File.new_for_uri (uri);
uint8[] contents;
file.load_contents (null, out contents, null);
var type = file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, 0);
var mime = type.get_content_type ();
debug ("Uploading %s (%s)", uri, mime);
label.label = _("Uploading...");
label.show ();
show ();
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 (accounts.formal.instance);
var msg = Soup.Form.request_new_from_multipart (url, multipart);
network.queue(msg, (sess, mess) => {
var root = network.parse (mess);
attachment = Attachment.parse (root);
editable = true;
rebind ();
debug ("Uploaded media: %lld", attachment.id);
});
}
catch (Error e) {
error (e.message);
app.error (_("File read error"), _("Can't read file %s: %s").printf (uri, e.message));
}
}
private bool on_clicked (EventButton ev){
if (ev.button == 8)
return false;
if (ev.button == 3)
return open_menu (ev.button, ev.time);
Desktop.open_uri (attachment.url);
return true;
}
public virtual bool open_menu (uint button, uint32 time) {
var menu = new Gtk.Menu ();
if (editable && attachment != null) {
var item_remove = new Gtk.MenuItem.with_label (_("Remove"));
item_remove.activate.connect (() => destroy ());
menu.add (item_remove);
menu.add (new Gtk.SeparatorMenuItem ());
}
var item_open_link = new Gtk.MenuItem.with_label (_("Open in Browser"));
item_open_link.activate.connect (() => Desktop.open_uri (attachment.url));
var item_copy_link = new Gtk.MenuItem.with_label (_("Copy Link"));
item_copy_link.activate.connect (() => Desktop.copy (attachment.url));
var item_download = new Gtk.MenuItem.with_label (_("Download"));
item_download.activate.connect (() => Desktop.download_file (attachment.url));
menu.add (item_open_link);
if (attachment.type != "unknown")
menu.add (item_download);
menu.add (new Gtk.SeparatorMenuItem ());
menu.add (item_copy_link);
menu.show_all ();
menu.attach_widget = this;
menu.popup_at_pointer ();
return true;
}
}

View File

@ -0,0 +1,182 @@
using Gtk;
using Gdk;
public class Tootle.Widgets.ImageAttachment : Gtk.DrawingArea {
public Attachment? attachment;
private bool editable = false;
private bool fill = false;
private Pixbuf? pixbuf = null;
private static Pixbuf? pixbuf_error;
private int center_x = 0;
private int center_y = 0;
construct {
if (pixbuf_error == null)
pixbuf_error = IconTheme.get_default ().load_icon ("image-missing", 32, IconLookupFlags.GENERIC_FALLBACK);
hexpand = true;
vexpand = true;
add_events (EventMask.BUTTON_PRESS_MASK);
draw.connect (on_draw);
button_press_event.connect (on_clicked);
}
public ImageAttachment (Attachment obj) {
attachment = obj;
network.load_pixbuf (attachment.preview_url, on_ready);
set_size_request (32, 128);
show_all ();
}
public ImageAttachment.upload (string uri) {
halign = Align.START;
valign = Align.START;
set_size_request (100, 100);
show_all ();
try {
GLib.File file = File.new_for_uri (uri);
uint8[] contents;
file.load_contents (null, out contents, null);
var type = file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, 0);
var mime = type.get_content_type ();
info ("Uploading %s (%s)", uri, mime);
show ();
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 (accounts.formal.instance);
var msg = Soup.Form.request_new_from_multipart (url, multipart);
network.queue (msg, (sess, mess) => {
var root = network.parse (mess);
attachment = Attachment.parse (root);
editable = true;
invalidate ();
network.load_pixbuf (attachment.preview_url, on_ready);
info ("Uploaded media: %lld", attachment.id);
});
}
catch (Error e) {
error (e.message);
app.error (_("File read error"), _("Can't read file %s: %s").printf (uri, e.message));
}
}
private void on_ready (Pixbuf? result) {
if (result == null)
result = pixbuf_error;
pixbuf = result;
invalidate ();
}
private void invalidate () {
var w = get_allocated_width ();
var h = get_allocated_height ();
if (fill) {
var h_scaled = (pixbuf.height * w) / pixbuf.width;
if (h_scaled > pixbuf.height) {
halign = Align.START;
set_size_request (pixbuf.width, pixbuf.height);
}
else {
halign = Align.FILL;
set_size_request (1, h_scaled);
}
}
queue_draw_area (0, 0, w, h);
}
private void calc_center (int w, int h, int size_w, int size_h, Cairo.Context? ctx = null) {
center_x = w/2 - size_w/2;
center_y = h/2 - size_h/2;
if (ctx != null)
ctx.translate (center_x, center_y);
}
public void fill_parent () {
fill = true;
size_allocate.connect (on_size_changed);
on_size_changed ();
}
public void on_size_changed () {
if (fill && pixbuf != null)
invalidate ();
}
private bool on_draw (Widget widget, Cairo.Context ctx) {
var w = widget.get_allocated_width ();
var h = widget.get_allocated_height ();
if (halign == Align.START) {
w = pixbuf.width;
h = pixbuf.height;
}
//Draw frame
ctx.set_source_rgba (1, 1, 1, 1);
Drawing.draw_rounded_rect (ctx, 0, 0, w, h, 4);
ctx.fill ();
//Draw image, spinner or an error icon
if (pixbuf != null) {
var thumbnail = Drawing.make_pixbuf_thumbnail (pixbuf, w, h, fill);
Drawing.draw_rounded_rect (ctx, 0, 0, w, h, 4);
calc_center (w, h, thumbnail.width, thumbnail.height, ctx);
Gdk.cairo_set_source_pixbuf (ctx, thumbnail, 0, 0);
ctx.fill ();
}
else {
calc_center (w, h, 32, 32, ctx);
set_state_flags (StateFlags.CHECKED, false); //Y U NO SPIN
get_style_context ().render_activity (ctx, 0, 0, 32, 32);
}
return false;
}
private bool on_clicked (EventButton ev){
switch (ev.button) {
case 3:
return open_menu (ev.button, ev.time);
case 1:
Desktop.open_uri (attachment.url);
return true;
}
return false;
}
public virtual bool open_menu (uint button, uint32 time) {
var menu = new Gtk.Menu ();
if (editable && attachment != null) {
var item_remove = new Gtk.MenuItem.with_label (_("Remove"));
item_remove.activate.connect (() => destroy ());
menu.add (item_remove);
menu.add (new Gtk.SeparatorMenuItem ());
}
var item_open_link = new Gtk.MenuItem.with_label (_("Open in Browser"));
item_open_link.activate.connect (() => Desktop.open_uri (attachment.url));
var item_copy_link = new Gtk.MenuItem.with_label (_("Copy Link"));
item_copy_link.activate.connect (() => Desktop.copy (attachment.url));
var item_download = new Gtk.MenuItem.with_label (_("Download"));
item_download.activate.connect (() => Desktop.download_file (attachment.url));
menu.add (item_open_link);
if (attachment.type != "unknown")
menu.add (item_download);
menu.add (new Gtk.SeparatorMenuItem ());
menu.add (item_copy_link);
menu.show_all ();
menu.attach_widget = this;
menu.popup_at_pointer ();
return true;
}
}

View File

@ -2,7 +2,7 @@ using Gtk;
using Granite;
public class Tootle.NotificationWidget : Grid {
private Notification notification;
public Separator? separator;
@ -13,7 +13,7 @@ public class Tootle.NotificationWidget : Grid {
construct {
margin = 6;
image = new Image.from_icon_name ("notification-symbolic", IconSize.BUTTON);
image.margin_start = 32;
image.margin_end = 6;
@ -27,7 +27,7 @@ public class Tootle.NotificationWidget : Grid {
notification.dismiss ();
destroy ();
});
attach (image, 1, 2);
attach (label, 2, 2);
attach (dismiss, 3, 2);
@ -39,25 +39,25 @@ public class Tootle.NotificationWidget : Grid {
image.icon_name = notification.type.get_icon ();
label.set_label (notification.type.get_desc (notification.account));
get_style_context ().add_class ("notification");
if (notification.status != null)
network.status_removed.connect (on_status_removed);
destroy.connect (() => {
if (separator != null)
separator.destroy ();
separator = null;
status_widget = null;
});
if (notification.status != null){
status_widget = new StatusWidget (notification.status);
status_widget = new StatusWidget (notification.status, true);
status_widget.is_notification = true;
status_widget.button_press_event.connect (status_widget.open);
status_widget.avatar.button_press_event.connect (status_widget.open_account);
attach (status_widget, 1, 3, 3, 1);
}
if (notification.type == NotificationType.FOLLOW_REQUEST) {
var box = new Box (Orientation.HORIZONTAL, 6);
box.margin_start = 32 + 16 + 8;
@ -65,10 +65,10 @@ public class Tootle.NotificationWidget : Grid {
box.pack_start (accept, false, false, 0);
var reject = new Button.with_label (_("Reject"));
box.pack_start (reject, false, false, 0);
attach (box, 1, 3, 3, 1);
box.show_all ();
accept.clicked.connect (() => {
destroy ();
notification.accept_follow_request ();
@ -79,12 +79,12 @@ public class Tootle.NotificationWidget : Grid {
});
}
}
private void on_status_removed (int64 id) {
if (id == notification.status.id) {
if (notification.type == NotificationType.WATCHLIST)
notification.dismiss ();
destroy ();
}
}

View File

@ -3,11 +3,11 @@ using Gdk;
using Granite;
public class Tootle.StatusWidget : Gtk.EventBox {
public Status status;
public bool is_notification = false;
public const int AVATAR_SIZE = 32;
public Separator? separator;
public Granite.Widgets.Avatar avatar;
protected Grid grid;
@ -19,9 +19,9 @@ public class Tootle.StatusWidget : Gtk.EventBox {
protected RichLabel? content_spoiler;
protected Button? spoiler_button;
protected Box title_box;
protected AttachmentBox attachments;
protected Widgets.AttachmentGrid attachments;
protected Image pin_indicator;
protected Box counters;
protected Label replies;
protected Label reblogs;
@ -32,40 +32,40 @@ public class Tootle.StatusWidget : Gtk.EventBox {
construct {
grid = new Grid ();
avatar = new Granite.Widgets.Avatar.with_default_icon (AVATAR_SIZE);
avatar.valign = Align.START;
avatar.margin_top = 6;
avatar.margin_start = 6;
avatar.margin_end = 6;
title_box = new Box (Gtk.Orientation.HORIZONTAL, 6);
title_box.hexpand = true;
title_box.margin_end = 12;
title_box.margin_top = 6;
title_user = new RichLabel ("");
title_box.pack_start (title_user, false, false, 0);
title_acct = new Gtk.Label ("");
title_acct.opacity = 0.5;
title_acct.ellipsize = Pango.EllipsizeMode.END;
title_box.pack_start (title_acct, false, false, 0);
title_date = new Gtk.Label ("");
title_date.opacity = 0.5;
title_date.ellipsize = Pango.EllipsizeMode.END;
title_box.pack_end (title_date, false, false, 0);
title_box.show_all ();
pin_indicator = new Image.from_icon_name ("view-pin-symbolic", IconSize.MENU);
pin_indicator.opacity = 0.5;
title_box.pack_end (pin_indicator, false, false, 0);
content_label = new RichLabel ("");
content_label.wrap_words ();
attachments = new AttachmentBox ();
attachments = new Widgets.AttachmentGrid ();
var revealer_box = new Box (Orientation.VERTICAL, 6);
revealer_box.margin_end = 12;
@ -74,11 +74,11 @@ public class Tootle.StatusWidget : Gtk.EventBox {
revealer = new Revealer ();
revealer.reveal_child = true;
revealer.add (revealer_box);
reblogs = new Label ("0");
favorites = new Label ("0");
replies = new Label ("0");
reblog = new ImageToggleButton ("media-playlist-repeat-symbolic");
reblog.set_action ();
reblog.tooltip_text = _("Boost");
@ -100,7 +100,7 @@ public class Tootle.StatusWidget : Gtk.EventBox {
reply.set_active (false);
PostDialog.reply (status.get_formal ());
});
counters = new Box (Orientation.HORIZONTAL, 6);
counters.margin_top = 6;
counters.margin_bottom = 6;
@ -111,43 +111,44 @@ public class Tootle.StatusWidget : Gtk.EventBox {
counters.add (reply);
counters.add (replies);
counters.show_all ();
add (grid);
grid.attach (avatar, 1, 1, 1, 4);
grid.attach (title_box, 2, 2, 1, 1);
grid.attach (revealer, 2, 4, 1, 1);
grid.attach (counters, 2, 5, 1, 1);
show_all ();
button_press_event.connect (on_clicked);
}
public StatusWidget (Status status) {
public StatusWidget (Status status, bool notification = false) {
this.status = status;
this.status.updated.connect (rebind);
if (this.status.reblog != null) {
is_notification = notification;
if (status.reblog != null) {
var image = new Image.from_icon_name("media-playlist-repeat-symbolic", IconSize.BUTTON);
image.halign = Align.END;
image.margin_end = 6;
image.margin_top = 6;
image.show ();
var label_text = _("<a href=\"%s\"><b>%s</b></a> boosted").printf (this.status.account.url, this.status.account.display_name);
var label = new RichLabel (label_text);
label.halign = Align.START;
label.margin_top = 6;
label.show ();
grid.attach (image, 1, 0, 1, 1);
grid.attach (label, 2, 0, 2, 1);
}
if (is_spoiler ()) {
revealer.reveal_child = false;
var spoiler_box = new Box (Orientation.HORIZONTAL, 6);
spoiler_box.margin_end = 12;
var spoiler_button_text = _("Toggle content");
if (status.sensitive && status.attachments != null) {
spoiler_button = new Button.from_icon_name ("mail-attachment-symbolic", Gtk.IconSize.BUTTON);
@ -161,129 +162,126 @@ public class Tootle.StatusWidget : Gtk.EventBox {
spoiler_button.hexpand = true;
spoiler_button.halign = Align.END;
spoiler_button.clicked.connect (() => revealer.set_reveal_child (!revealer.child_revealed));
var spoiler_text = _("[ This post contains sensitive content ]");
if (status.spoiler_text != null)
spoiler_text = status.spoiler_text;
content_spoiler = new RichLabel (spoiler_text);
content_spoiler.wrap_words ();
spoiler_box.add (content_spoiler);
spoiler_box.add (spoiler_button);
spoiler_box.show_all ();
grid.attach (spoiler_box, 2, 3, 1, 1);
}
if (status.get_formal ().attachments != null) {
attachments.clear ();
foreach (Attachment attachment in status.get_formal ().attachments)
attachments.append (attachment);
}
if (!is_notification && status.get_formal ().attachments != null)
attachments.pack (status.get_formal ().attachments);
else
attachments.destroy ();
destroy.connect (() => {
avatar.show_default (AVATAR_SIZE);
if (separator != null)
separator.destroy ();
});
network.status_removed.connect (id => {
if (id == status.id)
destroy ();
});
rebind ();
}
public void highlight () {
grid.get_style_context ().add_class ("card");
grid.margin_bottom = 6;
}
public int get_avatar_size () {
return AVATAR_SIZE * get_style_context ().get_scale ();
}
public void rebind () {
var formal = status.get_formal ();
title_user.set_label ("<b>%s</b>".printf ((formal.account.display_name)));
title_acct.label = "@" + formal.account.acct;
content_label.label = formal.content;
content_label.mentions = formal.mentions;
pin_indicator.visible = status.pinned;
var datetime = parse_date_iso8601 (formal.created_at);
title_date.label = Granite.DateTime.get_relative_datetime (datetime);
reblogs.label = formal.reblogs_count.to_string ();
favorites.label = formal.favourites_count.to_string ();
replies.label = formal.replies_count.to_string ();
reblog.sensitive = false;
reblog.active = formal.reblogged;
reblog.sensitive = true;
favorite.sensitive = false;
favorite.active = formal.favorited;
favorite.sensitive = true;
if (formal.visibility == StatusVisibility.DIRECT) {
reblog.sensitive = false;
reblog.icon.icon_name = formal.visibility.get_icon ();
reblog.tooltip_text = _("This post can't be boosted");
}
network.load_avatar (formal.account.avatar, avatar, get_avatar_size ());
}
public bool is_spoiler () {
return this.status.get_formal ().spoiler_text != null || this.status.get_formal ().sensitive;
}
private GLib.DateTime? parse_date_iso8601 (string date) {
var timeval = GLib.TimeVal ();
if (timeval.from_iso8601 (date))
return new GLib.DateTime.from_timeval_local (timeval);
return null;
}
public bool open_account (EventButton ev) {
if (ev.button == 8)
return false;
var view = new AccountView (status.get_formal ().account);
window.open_view (view);
return true;
}
public bool open (EventButton ev) {
if (ev.button == 8)
return false;
var formal = status.get_formal ();
var view = new StatusView (formal);
window.open_view (view);
return true;
}
private bool on_clicked (EventButton ev) {
if (ev.button == 8)
return false;
if (ev.button == 3)
return open_menu (ev.button, ev.time);
else
return false;
}
public virtual bool open_menu (uint button, uint32 time) {
var menu = new Gtk.Menu ();
var is_muted = status.muted;
var is_pinned = status.pinned;
var item_muting = new Gtk.MenuItem.with_label (is_muted ? _("Unmute Conversation") : _("Mute Conversation"));
item_muting.activate.connect (() => status.set_muted (!is_muted));
var item_open_link = new Gtk.MenuItem.with_label (_("Open in Browser"));
@ -295,31 +293,31 @@ public class Tootle.StatusWidget : Gtk.EventBox {
var sanitized = Html.remove_tags (status.get_formal ().content);
Desktop.copy (sanitized);
});
if (this.status.is_owned ()) {
var item_pin = new Gtk.MenuItem.with_label (is_pinned ? _("Unpin from Profile") : _("Pin on Profile"));
item_pin.activate.connect (() => status.set_pinned (!is_pinned));
menu.add (item_pin);
var item_delete = new Gtk.MenuItem.with_label (_("Delete"));
item_delete.activate.connect (() => status.poof ());
menu.add (item_delete);
var item_redraft = new Gtk.MenuItem.with_label (_("Redraft"));
item_redraft.activate.connect (() => PostDialog.redraft (status.get_formal ()));
menu.add (item_redraft);
menu.add (new Gtk.SeparatorMenuItem ());
}
if (this.is_notification)
menu.add (item_muting);
menu.add (item_open_link);
menu.add (new Gtk.SeparatorMenuItem ());
menu.add (item_copy_link);
menu.add (item_copy);
menu.show_all ();
menu.attach_widget = this;
menu.popup_at_pointer ();