Compose: Support media attachment

This commit is contained in:
Bleak Grey 2020-07-30 22:02:03 +03:00 committed by GitHub
parent 6e129ed663
commit 4a3df1f3fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1128 additions and 575 deletions

View File

@ -13,6 +13,7 @@
<file preprocess="xml-stripblanks">ui/widgets/list_item.ui</file>
<file preprocess="xml-stripblanks">ui/widgets/list_editor_item.ui</file>
<file preprocess="xml-stripblanks">ui/widgets/attachment_slot.ui</file>
<file preprocess="xml-stripblanks">ui/widgets/compose_attachment.ui</file>
<file preprocess="xml-stripblanks">ui/dialogs/compose.ui</file>
<file preprocess="xml-stripblanks">ui/dialogs/main.ui</file>
<file preprocess="xml-stripblanks">ui/dialogs/preferences.ui</file>

View File

@ -1,252 +1,401 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">dialog-warning-symbolic</property>
</object>
<object class="GtkImage" id="image2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">mail-attachment-symbolic</property>
</object>
<object class="GtkImage" id="image4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">face-smile-symbolic</property>
</object>
<template class="TootleDialogsCompose" parent="GtkWindow">
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="default_width">500</property>
<property name="default_height">250</property>
<property name="type_hint">dialog</property>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="has_subtitle">False</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkMenuButton" id="visibility_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImage" id="visibility_icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">pan-down-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="post_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="horizontal"/>
<class name="linked"/>
</style>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSpinner" id="spinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="box">
<property name="width_request">500</property>
<property name="height_request">250</property>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_bottom">8</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">8</property>
<child>
<object class="GtkRevealer" id="cw_revealer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkEntry" id="cw">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_top">8</property>
<property name="placeholder_text" translatable="yes">Write your warning here</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="width_request">350</property>
<property name="height_request">150</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="overlay_scrolling">False</property>
<child>
<object class="GtkTextView" id="content">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="pixels_below_lines">8</property>
<property name="pixels_inside_wrap">8</property>
<property name="wrap_mode">word-char</property>
<property name="left_margin">8</property>
<property name="right_margin">8</property>
<property name="top_margin">8</property>
<property name="bottom_margin">8</property>
<property name="accepts_tab">False</property>
<property name="populate_all">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox">
<object class="GtkStack" id="mode">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="transition_duration">100</property>
<property name="transition_type">crossfade</property>
<child>
<object class="GtkButton" id="emoji_button">
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="image">image4</property>
<property name="relief">none</property>
<property name="always_show_image">True</property>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="attach_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="image">image2</property>
<property name="relief">none</property>
<property name="always_show_image">True</property>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="cw_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="image">image1</property>
<property name="relief">none</property>
<property name="always_show_image">True</property>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="counter">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">250</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkRevealer" id="cw_revealer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkEntry" id="cw">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="margin_start">6</property>
<property name="margin_end">6</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="placeholder_text" translatable="yes">Write your warning here</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="width_request">350</property>
<property name="height_request">150</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="overlay_scrolling">False</property>
<child>
<object class="GtkTextView" id="content">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="pixels_below_lines">8</property>
<property name="pixels_inside_wrap">8</property>
<property name="wrap_mode">word-char</property>
<property name="left_margin">8</property>
<property name="right_margin">8</property>
<property name="top_margin">8</property>
<property name="bottom_margin">8</property>
<property name="accepts_tab">False</property>
<property name="populate_all">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkActionBar" id="actions">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkMenuButton" id="visibility_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Visibility</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImage" id="visibility_icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_end">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">pan-down-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<style>
<class name="flat"/>
</style>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="attach_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Attach Media</property>
<signal name="clicked" handler="on_select_media" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">mail-attachment-symbolic</property>
</object>
</child>
<style>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="emoji_button">
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Instance Emojis</property>
<property name="relief">none</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="image4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">face-smile-symbolic</property>
</object>
</child>
<style>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="cw_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Spoiler Warning</property>
<property name="relief">none</property>
<property name="always_show_image">True</property>
<signal name="toggled" handler="validate" swapped="no"/>
<child>
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">dialog-warning-symbolic</property>
</object>
</child>
<style>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="counter">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">250</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">3</property>
<property name="name">text</property>
<property name="title" translatable="yes">Text</property>
<property name="icon_name">edit</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="media_list">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">start</property>
<property name="hexpand">True</property>
<property name="selection_mode">none</property>
<signal name="row-activated" handler="on_media_list_row_activated" swapped="no"/>
<child>
<object class="GtkListBoxRow" id="attach_item">
<property name="height_request">64</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="icon_name">list-add-symbolic</property>
</object>
</child>
</object>
</child>
<style>
<class name="preferences"/>
</style>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="name">media</property>
<property name="title" translatable="yes">Media</property>
<property name="icon_name">mail-attachment-symbolic</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="app-view"/>
</style>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child type="title">
<object class="HdyViewSwitcherTitle" id="mode_switcher">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="policy">auto</property>
<property name="stack">mode</property>
</object>
</child>
<child>
<object class="GtkButton" id="commit">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="on_commit" swapped="no"/>
<child>
<object class="GtkStack" id="commit_stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="commit_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="name">ready</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">working</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="close">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="on_close" swapped="no"/>
</object>
<packing>
<property name="position">2</property>
</packing>
</child>
</object>

View File

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<template class="TootleDialogsComposeMediaItem" parent="GtkListBoxRow">
<property name="width_request">100</property>
<property name="height_request">80</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_start">6</property>
<property name="margin_end">6</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="row_spacing">6</property>
<property name="column_spacing">6</property>
<child>
<object class="GtkEntry" id="description">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="truncate_multiline">True</property>
<property name="caps_lock_warning">False</property>
<property name="placeholder_text" translatable="yes">Describe for the visually impaired</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">3</property>
</packing>
</child>
<child>
<object class="GtkStack" id="icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="icon_name">folder-documents-symbolic</property>
</object>
<packing>
<property name="name">new</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="icon_name">emblem-ok-symbolic</property>
</object>
<packing>
<property name="name">ok</property>
</packing>
</child>
<child>
<object class="GtkSpinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">upload</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="title_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Unknown</property>
<property name="ellipsize">start</property>
<property name="single_line_mode">True</property>
<property name="xalign">0</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="remove">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Delete</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="relief">none</property>
<signal name="clicked" handler="on_remove" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="margin_start">6</property>
<property name="margin_end">6</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="icon_name">user-trash-symbolic</property>
</object>
</child>
<style>
<class name="image-button"/>
<class name="flat"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
</child>
</template>
</interface>

View File

@ -1,13 +1,98 @@
public class Tootle.API.Attachment : Entity {
public string id { get; set; }
public string kind { get; set; }
public string url { get; set; }
public string? description { get; set; }
public string? _preview_url { get; set; }
public string? preview_url {
set { this._preview_url = value; }
get { return (this._preview_url == null || this._preview_url == "") ? url : _preview_url; }
// https://github.com/tootsuite/mastodon/blob/master/app/models/media_attachment.rb
public const string[] SUPPORTED_MIMES = {
"image/jpeg",
"image/png",
"image/gif",
"video/webm",
"video/mp4",
"video/quicktime",
"video/ogg",
"video/webm",
"video/quicktime",
"audio/wave",
"audio/wav",
"audio/x-wav",
"audio/x-pn-wave",
"audio/ogg",
"audio/mpeg",
"audio/mp3",
"audio/webm",
"audio/flac",
"audio/aac",
"audio/m4a",
"audio/x-m4a",
"audio/mp4",
"audio/3gpp",
"video/x-ms-asf"
};
public string id { get; set; }
public string kind { get; set; }
public string url { get; set; }
public string? description { get; set; }
public string? _preview_url { get; set; }
public string? preview_url {
set { this._preview_url = value; }
get { return (this._preview_url == null || this._preview_url == "") ? url : _preview_url; }
}
public static Attachment from (Json.Node node) throws Error {
return Entity.from_json (typeof (API.Attachment), node) as API.Attachment;
}
public static async Attachment upload (string uri, string title, string? descr) throws Error {
message (@"Uploading new media: $(uri)...");
uint8[] contents;
string mime;
GLib.FileInfo type;
try {
GLib.File file = File.new_for_uri (uri);
file.load_contents (null, out contents, null);
type = file.query_info (GLib.FileAttribute.STANDARD_CONTENT_TYPE, 0);
mime = type.get_content_type ();
}
catch (Error e) {
throw new Oopsie.USER (_("Can't open file $file:\n$reason")
.replace ("$file", title)
.replace ("$reason", e.message)
);
}
var descr_param = "";
if (descr != null && descr.replace (" ", "") != "") {
descr_param = "?description=" + Html.uri_encode (descr);
}
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 = @"$(accounts.active.instance)/api/v1/media$descr_param";
var msg = Soup.Form.request_new_from_multipart (url, multipart);
msg.request_headers.append ("Authorization", @"Bearer $(accounts.active.access_token)");
string? error = null;
network.queue (msg,
(sess, mess) => {
upload.callback ();
},
(code, reason) => {
error = reason;
upload.callback ();
});
yield;
if (error != null)
throw new Oopsie.INSTANCE (error);
else {
var node = network.parse_node (msg);
var entity = API.Attachment.from (node);
message (@"OK! ID $(entity.id)");
return entity;
}
}
}

View File

@ -81,6 +81,10 @@ public class Tootle.API.Status : Entity, Widgetizable {
return formal.account.id == accounts.active.id;
}
public bool has_media () {
return media_attachments != null && media_attachments.size > 0;
}
public string get_reply_mentions () {
var result = "";
if (account.acct != accounts.active.acct)
@ -111,15 +115,9 @@ public class Tootle.API.Status : Entity, Widgetizable {
.exec ();
}
public void poof (owned Soup.SessionCallback? cb = null, owned Network.ErrorCallback? err = network.on_error) {
new Request.DELETE (@"/api/v1/statuses/$id")
.with_account (accounts.active)
.then ((sess, msg) => {
streams.force_delete (id);
cb (sess, msg);
})
.on_error ((status, reason) => err (status, reason))
.exec ();
public Request annihilate () {
return new Request.DELETE (@"/api/v1/statuses/$id")
.with_account (accounts.active);
}
}

View File

@ -1,88 +1,147 @@
using Gtk;
using Gee;
[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/dialogs/compose.ui")]
public class Tootle.Dialogs.Compose : Window {
public API.Status? status { get; construct set; }
public string style_class { get; construct set; }
public string label { get; construct set; }
public int char_limit {
get {
return 500;
}
}
public API.Status? status { get; construct set; }
public string style_class { get; construct set; }
public string label { get; construct set; }
public bool working { get; set; default = false; }
public int char_limit {
get {
return 500;
}
}
[GtkChild]
protected Box box;
[GtkChild]
Hdy.ViewSwitcherTitle mode_switcher;
[GtkChild]
Button commit;
[GtkChild]
Stack commit_stack;
[GtkChild]
Label commit_label;
[GtkChild]
protected Revealer cw_revealer;
[GtkChild]
protected ToggleButton cw_button;
[GtkChild]
protected Entry cw;
[GtkChild]
protected Label counter;
[GtkChild]
Revealer cw_revealer;
[GtkChild]
ToggleButton cw_button;
[GtkChild]
Entry cw;
[GtkChild]
Label counter;
[GtkChild]
MenuButton visibility_button;
[GtkChild]
Image visibility_icon;
Widgets.VisibilityPopover visibility_popover;
[GtkChild]
TextView content;
[GtkChild]
protected MenuButton visibility_button;
[GtkChild]
protected Image visibility_icon;
protected Widgets.VisibilityPopover visibility_popover;
[GtkChild]
protected Button post_button;
[GtkChild]
Stack mode;
[GtkChild]
ListBox media_list;
[GtkChild]
protected TextView content;
[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/widgets/compose_attachment.ui")]
protected class MediaItem : Gtk.ListBoxRow {
construct {
transient_for = window;
Compose dialog;
public API.Attachment? entity { get; set; }
public string? source { get; set; }
post_button.label = label;
foreach (Widget w in new Widget[] { visibility_button, post_button })
w.get_style_context ().add_class (style_class);
[GtkChild]
public Label title_label;
[GtkChild]
public Entry description;
[GtkChild]
public Stack icon;
visibility_popover = new Widgets.VisibilityPopover.with_button (visibility_button);
visibility_popover.bind_property ("selected", visibility_icon, "icon-name", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
public MediaItem (Compose dialog, string? source, API.Attachment? entity) {
this.dialog = dialog;
this.source = source;
this.entity = entity;
if (source != null)
message (@"Attached uri: $source");
else {
message (@"Attached immutable $(entity.id)");
description.text = entity.description ?? " ";
description.sensitive = false;
}
dialog.set_media_mode (true);
title_label.label = GLib.Path.get_basename (source ?? entity.url).replace ("%20", " ");
}
[GtkCallback]
void on_remove () {
var remove = app.question (
_(@"Delete \"%s\"?").printf (title_label.label),
_("This action cannot be reverted."),
this.dialog
);
if (remove)
destroy ();
}
}
construct {
transient_for = window;
notify["working"].connect (on_state_change);
commit_label.label = label;
commit.get_style_context ().add_class (style_class);
visibility_popover = new Widgets.VisibilityPopover.with_button (visibility_button);
visibility_popover.bind_property ("selected", visibility_icon, "icon-name", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
target.set_string (((API.Visibility)src).get_icon ());
return true;
});
cw_button.bind_property ("active", cw_revealer, "reveal_child", BindingFlags.SYNC_CREATE);
cw_button.bind_property ("active", cw_revealer, "reveal_child", BindingFlags.SYNC_CREATE);
cw_button.toggled.connect (validate);
cw.buffer.deleted_text.connect (() => validate ());
cw.buffer.inserted_text.connect (() => validate ());
content.buffer.changed.connect (validate);
post_button.clicked.connect (on_post_button_clicked);
cw.buffer.deleted_text.connect (() => validate ());
cw.buffer.inserted_text.connect (() => validate ());
content.buffer.changed.connect (validate);
if (status.spoiler_text != null) {
cw.text = status.spoiler_text;
cw_button.active = true;
}
content.buffer.text = Html.remove_tags (status.content);
if (status.has_spoiler) {
cw.text = status.spoiler_text;
cw_button.active = true;
}
content.buffer.text = Html.remove_tags (status.content);
validate ();
show ();
}
set_media_mode (status.has_media ());
show ();
}
public Compose () {
Object (
status: new API.Status.empty (),
style_class: STYLE_CLASS_SUGGESTED_ACTION,
label: _("Post")
);
set_visibility (status.visibility);
}
public Compose () {
Object (
status: new API.Status.empty (),
style_class: STYLE_CLASS_SUGGESTED_ACTION,
label: _("Publish")
);
message ("Editing empty status");
set_visibility (status.visibility);
}
public Compose.redraft (API.Status status) {
Object (
status: status,
style_class: STYLE_CLASS_DESTRUCTIVE_ACTION,
label: _("Redraft")
);
set_visibility (status.visibility);
}
public Compose.redraft (API.Status status) {
Object (
status: status,
style_class: STYLE_CLASS_DESTRUCTIVE_ACTION,
label: _("Redraft")
);
set_visibility (status.visibility);
message (@"Redrafting status $(status.id)");
status.media_attachments.@foreach (a => {
media_list.insert (new MediaItem (this, null, a), 0);
return true;
});
}
public Compose.reply (API.Status to) {
var template = new API.Status.empty ();
@ -90,76 +149,166 @@ public class Tootle.Dialogs.Compose : Window {
template.in_reply_to_account_id = to.account.id.to_string ();
template.content = to.formal.get_reply_mentions ();
Object (
status: template,
style_class: STYLE_CLASS_SUGGESTED_ACTION,
label: _("Reply")
status: template,
style_class: STYLE_CLASS_SUGGESTED_ACTION,
label: _("Reply")
);
set_visibility (to.visibility);
message (@"Replying to status $(status.in_reply_to_id)");
}
void set_visibility (API.Visibility v) {
visibility_popover.selected = v;
visibility_popover.invalidate ();
}
void set_visibility (API.Visibility v) {
visibility_popover.selected = v;
visibility_popover.invalidate ();
}
void validate () {
var remain = char_limit - content.buffer.get_char_count ();
if (cw_button.active)
remain -= (int) cw.buffer.get_length ();
void set_media_mode (bool has_media) {
mode_switcher.view_switcher_enabled = has_media;
}
counter.label = remain.to_string ();
post_button.sensitive = remain >= 0;
visibility_button.sensitive = true;
box.sensitive = true;
}
[GtkCallback]
void validate () {
var remain = char_limit - content.buffer.get_char_count ();
if (cw_button.active)
remain -= (int) cw.buffer.get_length ();
void on_error (int32 code, string reason) { //TODO: display errors
warning (reason);
validate ();
}
counter.label = remain.to_string ();
commit.sensitive = remain >= 0;
}
void on_post_button_clicked () {
post_button.sensitive = false;
visibility_button.sensitive = false;
box.sensitive = false;
void on_state_change (ParamSpec? p) {
commit_stack.visible_child_name = working ? "working" : "ready";
commit.sensitive = !working;
if (status.id != "") {
info ("Removing old status...");
status.poof (publish, on_error);
}
else {
publish ();
}
}
media_list.@foreach (w => {
var item = w as MediaItem;
if (item != null)
item.icon.visible_child_name = working ? "upload" : "new";
});
}
void publish () {
info ("Publishing new status...");
status.content = content.buffer.text;
status.spoiler_text = cw.text;
[GtkCallback]
void on_select_media () {
var filter = new Gtk.FileFilter ();
foreach (string mime in API.Attachment.SUPPORTED_MIMES)
filter.add_mime_type (mime);
var req = new Request.POST ("/api/v1/statuses")
.with_account (accounts.active)
.with_param ("visibility", visibility_popover.selected.to_string ())
.with_param ("status", Html.uri_encode (status.content));
var chooser = new Gtk.FileChooserNative (
_("Select media"),
this,
Gtk.FileChooserAction.OPEN,
_("_Open"),
_("_Cancel")
);
chooser.select_multiple = true;
chooser.set_filter (filter);
if (cw_button.active) {
req.with_param ("sensitive", "true");
req.with_param ("spoiler_text", Html.uri_encode (cw.text));
}
if (chooser.run () == Gtk.ResponseType.ACCEPT) {
foreach (unowned string uri in chooser.get_uris ())
media_list.insert (new MediaItem (this, uri, null), 0);
if (status.in_reply_to_id != null)
req.with_param ("in_reply_to_id", status.in_reply_to_id);
if (status.in_reply_to_account_id != null)
req.with_param ("in_reply_to_account_id", status.in_reply_to_account_id);
mode.visible_child_name = "media";
}
}
req.then ((sess, mess) => {
var node = network.parse_node (mess);
var status = API.Status.from (node);
info ("OK: status id is %s", status.id.to_string ());
destroy ();
})
.on_error (on_error)
.exec ();
}
[GtkCallback]
void on_media_list_row_activated (Widget w) {
if (!(w is MediaItem))
on_select_media ();
}
[GtkCallback]
void on_close () {
destroy ();
}
void on_error (int32 code, string reason) { //TODO: display errors
warning (reason);
working = false;
}
[GtkCallback]
void on_commit () {
working = true;
transaction.begin ((obj, res) => {
try {
transaction.end (res);
on_close ();
}
catch (Error e) {
working = false;
on_error (0, e.message);
}
});
}
async void transaction () throws Error {
if (status.id != "") {
message ("Removing old status...");
yield status.annihilate ().await ();
}
Gee.ArrayList<MediaItem> pending_media = new Gee.ArrayList<MediaItem>();
Gee.ArrayList<string> media_ids = new Gee.ArrayList<string>();
media_list.@foreach (w => {
var item = w as MediaItem;
if (item != null)
pending_media.add (item);
});
var media_param = "";
if (!pending_media.is_empty) {
message (@"Processing $(pending_media.size) attachments...");
if (!status.has_media ())
status.media_attachments = new ArrayList<API.Attachment>();
foreach (MediaItem item in pending_media) {
if (item.entity != null) {
message (@"Adding immutable media: $(item.entity.id)...");
media_ids.add (item.entity.id);
}
else {
mode.visible_child_name = "media";
var entity = yield API.Attachment.upload (
item.source,
item.title_label.label,
item.description.text);
media_ids.add (entity.id);
}
item.icon.visible_child_name = "ok";
}
media_param = Request.array2string (media_ids, "media_ids");
media_param += "&";
}
message ("Publishing status...");
status.content = content.buffer.text;
status.spoiler_text = cw.text;
var req = new Request.POST (@"/api/v1/statuses?$media_param")
.with_account (accounts.active)
.with_param ("visibility", visibility_popover.selected.to_string ())
.with_param ("status", Html.uri_encode (status.content));
if (cw_button.active) {
req.with_param ("sensitive", "true");
req.with_param ("spoiler_text", Html.uri_encode (cw.text));
}
if (status.in_reply_to_id != null)
req.with_param ("in_reply_to_id", status.in_reply_to_id);
if (status.in_reply_to_account_id != null)
req.with_param ("in_reply_to_account_id", status.in_reply_to_account_id);
yield req.await ();
var node = network.parse_node (req);
var status = API.Status.from (node);
message (@"OK: Published with ID $(status.id)");
on_close ();
}
}

View File

@ -224,9 +224,9 @@ public class Tootle.Dialogs.ListEditor: Gtk.Window {
[GtkCallback]
void on_save_clicked () {
working = true;
exec_save_chain.begin ((obj, res) => {
transaction.begin ((obj, res) => {
try {
exec_save_chain.end (res);
transaction.end (res);
done ();
destroy ();
@ -238,7 +238,7 @@ public class Tootle.Dialogs.ListEditor: Gtk.Window {
});
}
async void exec_save_chain () throws Error {
async void transaction () throws Error {
if (!exists) {
message ("Creating list...");
var req = new Request.POST ("/api/v1/lists")

View File

@ -1,30 +1,45 @@
public class Tootle.Html {
public static string remove_tags (string content) {
var all_tags = new Regex ("<(.|\n)*?>", RegexCompileFlags.CASELESS);
return GLib.Markup.escape_text (all_tags.replace (content, -1, 0, ""));
}
public const string FALLBACK_TEXT = _("[ There was an error parsing this text :c ]");
public static string simplify (string str) {
var divided = str
.replace("<br>", "\n")
.replace("</br>", "")
.replace("<br />", "\n")
.replace("<p>", "")
.replace("</p>", "\n\n");
public static string remove_tags (string content) {
try {
var fixed_paragraphs = simplify (content);
var all_tags = new Regex ("<(.|\n)*?>", RegexCompileFlags.CASELESS);
return Widgets.RichLabel.restore_entities (all_tags.replace (fixed_paragraphs, -1, 0, ""));
}
catch (Error e) {
warning (e.message);
return FALLBACK_TEXT;
}
}
var html_params = new Regex ("(class|target|rel)=\"(.|\n)*?\"", RegexCompileFlags.CASELESS);
var simplified = html_params.replace (divided, -1, 0, "");
public static string simplify (string str) {
try {
var divided = str
.replace("<br>", "\n")
.replace("</br>", "")
.replace("<br />", "\n")
.replace("<p>", "")
.replace("</p>", "\n\n");
while (simplified.has_suffix ("\n"))
simplified = simplified.slice (0, simplified.last_index_of ("\n"));
var html_params = new Regex ("(class|target|rel)=\"(.|\n)*?\"", RegexCompileFlags.CASELESS);
var simplified = html_params.replace (divided, -1, 0, "");
return simplified;
}
while (simplified.has_suffix ("\n"))
simplified = simplified.slice (0, simplified.last_index_of ("\n"));
public static string uri_encode (string str) {
var restored = Widgets.RichLabel.restore_entities (str);
return Soup.URI.encode (restored, ";&+");
}
return simplified;
}
catch (Error e) {
warning (e.message);
return FALLBACK_TEXT;
}
}
public static string uri_encode (string str) {
var restored = Widgets.RichLabel.restore_entities (str);
return Soup.URI.encode (restored, ";&+");
}
}

View File

@ -101,9 +101,6 @@ public class Tootle.Request : Soup.Message {
url = account.instance + url;
uri = new URI (url + parameters);
url = uri.to_string (false);
message (@"$method: $url");
network.queue (this, (owned) cb, (owned) error_cb);
return this;
}

View File

@ -43,12 +43,14 @@ public class Tootle.Network : GLib.Object {
session.cancel_message (msg, Soup.Status.CANCELLED);
}
public void queue (owned Soup.Message message, owned SuccessCallback cb, owned ErrorCallback ecb) {
public void queue (owned Soup.Message mess, owned SuccessCallback cb, owned ErrorCallback ecb) {
requests_processing++;
started ();
message (@"$(mess.method): $(mess.uri.to_string (false))");
try {
session.queue_message (message, (sess, msg) => {
session.queue_message (mess, (sess, msg) => {
var status = msg.status_code;
if (status == Soup.Status.OK)
cb (session, msg);

View File

@ -138,8 +138,8 @@ public class Tootle.Streams : Object {
decode (bytes, out root, out root_obj, out ev);
// c.subscribers.@foreach (s => {
// warning ("%s: %s for %s", c.name, e, get_subscriber_name (s));
// return false;
// message (@"$(c.name): $ev for $(get_subscriber_name (s))");
// return true;
// });
switch (ev) {
@ -176,7 +176,7 @@ public class Tootle.Streams : Object {
connections.get_values ().@foreach (c => {
c.subscribers.@foreach (s => {
s.on_status_removed (id);
return false;
return true;
});
});
}

View File

@ -28,11 +28,11 @@ public class Tootle.Views.Lists : Views.Timeline {
[GtkCallback]
void on_remove_clicked () {
var yes = app.question (
var remove = app.question (
_("Delete \"%s\"?").printf (list.title),
_("This action cannot be reverted.")
);
if (yes) {
if (remove) {
new Request.DELETE (@"/api/v1/lists/$(list.id)")
.with_account (accounts.active)
.then (() => { this.destroy (); })

View File

@ -155,7 +155,7 @@ public class Tootle.Views.Timeline : IAccountListener, IStreamListener, Views.Ba
protected virtual void remove_status (string id) {
if (settings.live_updates) {
content.get_children ().@foreach (w => {
content_list.get_children ().@foreach (w => {
var sw = w as Widgets.Status;
if (sw != null && sw.status.id == id)
sw.destroy ();

View File

@ -4,71 +4,71 @@ using Gdk;
[GtkTemplate (ui = "/com/github/bleakgrey/tootle/ui/widgets/status.ui")]
public class Tootle.Widgets.Status : ListBoxRow {
public API.Status status { get; construct set; }
public API.NotificationType? kind { get; construct set; }
public API.Status status { get; construct set; }
public API.NotificationType? kind { get; construct set; }
[GtkChild]
protected Grid grid;
[GtkChild]
protected Grid grid;
[GtkChild]
protected Image header_icon;
[GtkChild]
protected Widgets.RichLabel header_label;
[GtkChild]
protected Image header_icon;
[GtkChild]
protected Widgets.RichLabel header_label;
[GtkChild]
public Widgets.Avatar avatar;
[GtkChild]
protected Widgets.RichLabel name_label;
[GtkChild]
protected Widgets.RichLabel handle_label;
[GtkChild]
protected Widgets.RichLabel date_label;
[GtkChild]
protected Image pin_indicator;
[GtkChild]
public Revealer revealer;
[GtkChild]
protected Widgets.RichLabel content;
[GtkChild]
protected Widgets.RichLabel revealer_content;
[GtkChild]
protected Widgets.Attachment.Box attachments;
[GtkChild]
public Widgets.Avatar avatar;
[GtkChild]
protected Widgets.RichLabel name_label;
[GtkChild]
protected Widgets.RichLabel handle_label;
[GtkChild]
protected Widgets.RichLabel date_label;
[GtkChild]
protected Image pin_indicator;
[GtkChild]
public Revealer revealer;
[GtkChild]
protected Widgets.RichLabel content;
[GtkChild]
protected Widgets.RichLabel revealer_content;
[GtkChild]
protected Widgets.Attachment.Box attachments;
[GtkChild]
protected Box actions;
[GtkChild]
protected Button reply_button;
[GtkChild]
protected ToggleButton reblog_button;
[GtkChild]
protected Image reblog_icon;
[GtkChild]
protected ToggleButton favorite_button;
[GtkChild]
protected ToggleButton bookmark_button;
[GtkChild]
protected Button menu_button;
[GtkChild]
protected Box actions;
[GtkChild]
protected Button reply_button;
[GtkChild]
protected ToggleButton reblog_button;
[GtkChild]
protected Image reblog_icon;
[GtkChild]
protected ToggleButton favorite_button;
[GtkChild]
protected ToggleButton bookmark_button;
[GtkChild]
protected Button menu_button;
protected string escaped_spoiler {
owned get {
if (status.formal.has_spoiler) {
var text = status.formal.spoiler_text ?? "";
var label = _("[ Toggle content ]");
text += @" <a href=\"tootle://toggle\">$label</a>";
return text;
}
else
return status.formal.content;
}
}
protected string escaped_spoiler {
owned get {
if (status.formal.has_spoiler) {
var text = status.formal.spoiler_text ?? "";
var label = _("[ Toggle content ]");
text += @" <a href=\"tootle://toggle\">$label</a>";
return text;
}
else
return status.formal.content;
}
}
protected string escaped_content {
owned get {
return status.formal.has_spoiler ? status.formal.content : "";
}
}
protected string escaped_content {
owned get {
return status.formal.has_spoiler ? status.formal.content : "";
}
}
protected string display_name {
protected string display_name {
owned get {
var name = Html.simplify (status.formal.account.display_name);
return @"<b>$name</b>";
@ -77,166 +77,172 @@ public class Tootle.Widgets.Status : ListBoxRow {
protected string date {
owned get {
var date = new GLib.DateTime.from_iso8601 (status.formal.created_at, null);
var humanized = Granite.DateTime.get_relative_datetime (date);
return @"<small>$humanized</small>";
var date = new GLib.DateTime.from_iso8601 (status.formal.created_at, null);
var humanized = Granite.DateTime.get_relative_datetime (date);
return @"<small>$humanized</small>";
}
}
protected string handle {
protected string handle {
owned get {
return @"<small>$(status.formal.account.handle)</small>";
}
}
public virtual signal void open () {
if (status.id == "") {
var view = new Views.Profile (status.formal.account);
window.open_view (view);
}
else {
var formal = status.formal;
var view = new Views.ExpandedStatus (formal);
window.open_view (view);
}
}
public virtual signal void open () {
if (status.id == "") {
var view = new Views.Profile (status.formal.account);
window.open_view (view);
}
else {
var formal = status.formal;
var view = new Views.ExpandedStatus (formal);
window.open_view (view);
}
}
construct {
content.activate_link.connect (on_toggle_spoiler);
notify["kind"].connect (on_kind_changed);
construct {
content.activate_link.connect (on_toggle_spoiler);
notify["kind"].connect (on_kind_changed);
if (kind == null) {
if (status.reblog != null)
kind = API.NotificationType.REBLOG_REMOTE_USER;
}
if (kind == null) {
if (status.reblog != null)
kind = API.NotificationType.REBLOG_REMOTE_USER;
}
status.formal.bind_property ("favourited", favorite_button, "active", BindingFlags.SYNC_CREATE);
favorite_button.clicked.connect (() => {
status.action (status.formal.favourited ? "unfavourite" : "favourite");
});
status.formal.bind_property ("favourited", favorite_button, "active", BindingFlags.SYNC_CREATE);
favorite_button.clicked.connect (() => {
status.action (status.formal.favourited ? "unfavourite" : "favourite");
});
status.formal.bind_property ("reblogged", reblog_button, "active", BindingFlags.SYNC_CREATE);
reblog_button.clicked.connect (() => {
status.action (status.formal.reblogged ? "unreblog" : "reblog");
});
status.formal.bind_property ("reblogged", reblog_button, "active", BindingFlags.SYNC_CREATE);
reblog_button.clicked.connect (() => {
status.action (status.formal.reblogged ? "unreblog" : "reblog");
});
status.formal.bind_property ("bookmarked", bookmark_button, "active", BindingFlags.SYNC_CREATE);
bookmark_button.clicked.connect (() => {
status.action (status.formal.bookmarked ? "unbookmark" : "bookmark");
});
status.formal.bind_property ("bookmarked", bookmark_button, "active", BindingFlags.SYNC_CREATE);
bookmark_button.clicked.connect (() => {
status.action (status.formal.bookmarked ? "unbookmark" : "bookmark");
});
reply_button.clicked.connect (() => new Dialogs.Compose.reply (status));
reply_button.clicked.connect (() => new Dialogs.Compose.reply (status));
bind_property ("escaped-spoiler", content, "text", BindingFlags.SYNC_CREATE);
bind_property ("escaped-content", revealer_content, "text", BindingFlags.SYNC_CREATE);
status.formal.account.bind_property ("avatar", avatar, "url", BindingFlags.SYNC_CREATE);
bind_property ("escaped-spoiler", content, "text", BindingFlags.SYNC_CREATE);
bind_property ("escaped-content", revealer_content, "text", BindingFlags.SYNC_CREATE);
status.formal.account.bind_property ("avatar", avatar, "url", BindingFlags.SYNC_CREATE);
bind_property ("handle", handle_label, "label", BindingFlags.SYNC_CREATE);
bind_property ("display_name", name_label, "text", BindingFlags.SYNC_CREATE);
bind_property ("date", date_label, "label", BindingFlags.SYNC_CREATE);
status.formal.bind_property ("pinned", pin_indicator, "visible", BindingFlags.SYNC_CREATE);
status.formal.bind_property ("has_spoiler", revealer_content, "visible", BindingFlags.SYNC_CREATE);
revealer.reveal_child = !status.formal.has_spoiler;
status.formal.bind_property ("has_spoiler", revealer_content, "visible", BindingFlags.SYNC_CREATE);
revealer.reveal_child = !status.formal.has_spoiler;
if (status.formal.visibility == API.Visibility.DIRECT) {
reblog_icon.icon_name = status.formal.visibility.get_icon ();
reblog_button.sensitive = false;
reblog_button.tooltip_text = _("This post can't be boosted");
}
if (status.formal.visibility == API.Visibility.DIRECT) {
reblog_icon.icon_name = status.formal.visibility.get_icon ();
reblog_button.sensitive = false;
reblog_button.tooltip_text = _("This post can't be boosted");
}
if (status.id == "") {
actions.destroy ();
date_label.destroy ();
content.single_line_mode = true;
content.lines = 2;
content.ellipsize = Pango.EllipsizeMode.END;
button_release_event.connect (on_avatar_clicked);
}
if (status.id == "") {
actions.destroy ();
date_label.destroy ();
content.single_line_mode = true;
content.lines = 2;
content.ellipsize = Pango.EllipsizeMode.END;
button_release_event.connect (on_avatar_clicked);
}
if (!attachments.populate (status.formal.media_attachments) || status.id == "") {
attachments.destroy ();
}
if (!attachments.populate (status.formal.media_attachments) || status.id == "") {
attachments.destroy ();
}
menu_button.clicked.connect (open_menu);
avatar.button_release_event.connect (on_avatar_clicked);
}
menu_button.clicked.connect (open_menu);
avatar.button_release_event.connect (on_avatar_clicked);
}
public Status (API.Status status, API.NotificationType? _kind = null) {
Object (status: status, kind: _kind);
}
~Status () {
notify["kind"].disconnect (on_kind_changed);
}
public Status (API.Status status, API.NotificationType? _kind = null) {
Object (status: status, kind: _kind);
}
~Status () {
notify["kind"].disconnect (on_kind_changed);
}
protected bool on_toggle_spoiler (string uri) {
if (uri == "tootle://toggle") {
revealer.reveal_child = !revealer.reveal_child;
return true;
}
return false;
}
protected bool on_toggle_spoiler (string uri) {
if (uri == "tootle://toggle") {
revealer.reveal_child = !revealer.reveal_child;
return true;
}
return false;
}
protected virtual void on_kind_changed () {
header_icon.visible = header_label.visible = (kind != null);
if (kind == null)
return;
protected virtual void on_kind_changed () {
header_icon.visible = header_label.visible = (kind != null);
if (kind == null)
return;
header_icon.icon_name = kind.get_icon ();
header_label.label = kind.get_desc (status.account);
}
header_icon.icon_name = kind.get_icon ();
header_label.label = kind.get_desc (status.account);
}
public bool on_avatar_clicked (EventButton ev) {
if (ev.button == 1 && ev.type == EventType.BUTTON_RELEASE) {
var view = new Views.Profile (status.formal.account);
return window.open_view (view);
}
return false;
}
public bool on_avatar_clicked (EventButton ev) {
if (ev.button == 1 && ev.type == EventType.BUTTON_RELEASE) {
var view = new Views.Profile (status.formal.account);
return window.open_view (view);
}
return false;
}
protected void open_menu () {
var menu = new Gtk.Menu ();
protected void open_menu () {
var menu = new Gtk.Menu ();
var item_open_link = new Gtk.MenuItem.with_label (_("Open in Browser"));
item_open_link.activate.connect (() => Desktop.open_uri (status.formal.url));
var item_copy_link = new Gtk.MenuItem.with_label (_("Copy Link"));
item_copy_link.activate.connect (() => Desktop.copy (status.formal.url));
var item_copy = new Gtk.MenuItem.with_label (_("Copy Text"));
item_copy.activate.connect (() => {
var sanitized = Html.remove_tags (status.formal.content);
Desktop.copy (sanitized);
});
var item_open_link = new Gtk.MenuItem.with_label (_("Open in Browser"));
item_open_link.activate.connect (() => Desktop.open_uri (status.formal.url));
var item_copy_link = new Gtk.MenuItem.with_label (_("Copy Link"));
item_copy_link.activate.connect (() => Desktop.copy (status.formal.url));
var item_copy = new Gtk.MenuItem.with_label (_("Copy Text"));
item_copy.activate.connect (() => {
var sanitized = Html.remove_tags (status.formal.content);
Desktop.copy (sanitized);
});
// if (is_notification) {
// var item_muting = new Gtk.MenuItem.with_label (status.muted ? _("Unmute Conversation") : _("Mute Conversation"));
// item_muting.activate.connect (() => status.update_muted (!is_muted) );
// menu.add (item_muting);
// }
// if (is_notification) {
// var item_muting = new Gtk.MenuItem.with_label (status.muted ? _("Unmute Conversation") : _("Mute Conversation"));
// item_muting.activate.connect (() => status.update_muted (!is_muted) );
// menu.add (item_muting);
// }
menu.add (item_open_link);
menu.add (new SeparatorMenuItem ());
menu.add (item_copy_link);
menu.add (item_copy);
menu.add (item_open_link);
menu.add (new SeparatorMenuItem ());
menu.add (item_copy_link);
menu.add (item_copy);
if (status.is_owned ()) {
menu.add (new SeparatorMenuItem ());
if (status.is_owned ()) {
menu.add (new SeparatorMenuItem ());
var item_pin = new Gtk.MenuItem.with_label (status.pinned ? _("Unpin from Profile") : _("Pin on Profile"));
item_pin.activate.connect (() => {
status.action (status.formal.pinned ? "unpin" : "pin");
});
menu.add (item_pin);
var item_pin = new Gtk.MenuItem.with_label (status.pinned ? _("Unpin from Profile") : _("Pin on Profile"));
item_pin.activate.connect (() => {
status.action (status.formal.pinned ? "unpin" : "pin");
});
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_delete = new Gtk.MenuItem.with_label (_("Delete"));
item_delete.activate.connect (() => {
status.annihilate ()
.then ((sess, mess) => {
streams.force_delete (status.id);
})
.exec ();
});
menu.add (item_delete);
var item_redraft = new Gtk.MenuItem.with_label (_("Redraft"));
item_redraft.activate.connect (() => new Dialogs.Compose.redraft (status.formal));
menu.add (item_redraft);
}
var item_redraft = new Gtk.MenuItem.with_label (_("Redraft"));
item_redraft.activate.connect (() => new Dialogs.Compose.redraft (status.formal));
menu.add (item_redraft);
}
menu.show_all ();
menu.popup_at_widget (menu_button, Gravity.SOUTH_EAST, Gravity.SOUTH_EAST);
}
menu.show_all ();
menu.popup_at_widget (menu_button, Gravity.SOUTH_EAST, Gravity.SOUTH_EAST);
}
}

View File

@ -9,7 +9,7 @@ public class Tootle.Widgets.VisibilityPopover: Popover {
construct {
var box = new Box (Orientation.VERTICAL, 8);
box.margin = 8;
box.margin = 12;
box.show ();
add (box);