diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 44cf3c6a..cd504810 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -47,7 +47,7 @@ set(RESOURCE_LIST settings_dialog.ui unified_window_placeholder.ui - pre_theme.css + theme.css ) compile_gresources( @@ -85,6 +85,7 @@ SOURCES src/ui/add_conversation/select_jid_fragment.vala src/ui/avatar_generator.vala src/ui/chat_input/edit_history.vala + src/ui/chat_input/encryption_button.vala src/ui/chat_input/occupants_tab_completer.vala src/ui/chat_input/smiley_converter.vala src/ui/chat_input/view.vala @@ -107,7 +108,6 @@ SOURCES src/ui/conversation_summary/message_populator.vala src/ui/conversation_summary/message_textview.vala src/ui/conversation_summary/slashme_message_display.vala - src/ui/conversation_titlebar/encryption_entry.vala src/ui/conversation_titlebar/menu_entry.vala src/ui/conversation_titlebar/occupants_entry.vala src/ui/conversation_titlebar/view.vala diff --git a/main/data/chat_input.ui b/main/data/chat_input.ui index 2436ff82..9a8cf424 100644 --- a/main/data/chat_input.ui +++ b/main/data/chat_input.ui @@ -6,6 +6,9 @@ horizontal 5 True + 300 @@ -15,12 +18,17 @@ True - - GTK_WRAP_WORD_CHAR - 5 - True - True + True + + + GTK_WRAP_WORD_CHAR + 5 + True + True + True + + diff --git a/main/data/conversation_summary/message_item.ui b/main/data/conversation_summary/message_item.ui index 8d53a691..a6937d10 100644 --- a/main/data/conversation_summary/message_item.ui +++ b/main/data/conversation_summary/message_item.ui @@ -38,7 +38,7 @@ - + False 1 start @@ -54,7 +54,7 @@ - + False 1 start diff --git a/main/data/menu_encryption.ui b/main/data/menu_encryption.ui index 7aae53ee..9e63b17d 100644 --- a/main/data/menu_encryption.ui +++ b/main/data/menu_encryption.ui @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/main/data/pre_theme.css b/main/data/pre_theme.css deleted file mode 100644 index 392e603f..00000000 --- a/main/data/pre_theme.css +++ /dev/null @@ -1,10 +0,0 @@ -/** - * This theme file is applied before the operating system theme and any user configuration. - * It provides sane defaults for things that are very Dino-specific. - */ - -window.dino-main headerbar.dino-left label.title { - opacity: 0; - font-size: 0; - color: transparent; -} \ No newline at end of file diff --git a/main/data/theme.css b/main/data/theme.css new file mode 100644 index 00000000..d2723d54 --- /dev/null +++ b/main/data/theme.css @@ -0,0 +1,45 @@ +/** + * This theme file is applied after the operating system theme + * It provides sane defaults for things that are very Dino-specific. + */ + +window.dino-main headerbar.dino-left label.title { + opacity: 0; + font-size: 0; + color: transparent; +} + +window.dino-main .dino-chatinput frame box { + background: @theme_base_color; +} + +window.dino-main .dino-chatinput frame box:backdrop { + background: @theme_unfocused_base_color; +} + +window.dino-main button.dino-chatinput-button { + border: none; + background: transparent; + box-shadow: none; + min-height: 0; + padding: 7px 5px; + color: alpha(@theme_fg_color, 0.6); + outline: none; +} + +window.dino-main button.dino-chatinput-button:hover { + color: @theme_selected_bg_color; +} + +window.dino-main button.dino-chatinput-button:backdrop { + color: alpha(@theme_unfocused_fg_color, 0.6); +} + +window.dino-main button.dino-chatinput-button:active, +window.dino-main button.dino-chatinput-button:checked { + color: alpha(@theme_selected_bg_color, 0.8); +} + +window.dino-main button.dino-chatinput-button:checked:backdrop { + color: alpha(@theme_unfocused_selected_bg_color, 0.8); +} diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala index 708c63b0..65bb8743 100644 --- a/main/src/ui/application.vala +++ b/main/src/ui/application.vala @@ -21,8 +21,8 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application { Window.set_default_icon_name("dino"); CssProvider provider = new CssProvider(); - provider.load_from_resource("/im/dino/pre_theme.css"); - StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, STYLE_PROVIDER_PRIORITY_THEME - 1); + provider.load_from_resource("/im/dino/theme.css"); + StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, STYLE_PROVIDER_PRIORITY_APPLICATION); activate.connect(() => { if (window == null) { diff --git a/main/src/ui/conversation_titlebar/encryption_entry.vala b/main/src/ui/chat_input/encryption_button.vala similarity index 63% rename from main/src/ui/conversation_titlebar/encryption_entry.vala rename to main/src/ui/chat_input/encryption_button.vala index 16cc5fdd..6ae3e8af 100644 --- a/main/src/ui/conversation_titlebar/encryption_entry.vala +++ b/main/src/ui/chat_input/encryption_button.vala @@ -5,32 +5,26 @@ using Dino.Entities; namespace Dino.Ui { -class EncryptionEntry : Plugins.ConversationTitlebarEntry, Object { - public string id { get { return "encryption"; } } - - public double order { get { return 2; } } - public Plugins.ConversationTitlebarWidget get_widget(Plugins.WidgetType type) { - if (type == Plugins.WidgetType.GTK) { - return new EncryptionWidget() { visible=true }; - } - return null; - } -} - -class EncryptionWidget : MenuButton, Plugins.ConversationTitlebarWidget { +public class EncryptionButton : MenuButton { private Conversation? conversation; private RadioButton? button_unencrypted; private Map encryption_radios = new HashMap(); - public EncryptionWidget() { + public EncryptionButton() { + relief = ReliefStyle.NONE; + use_popover = true; + image = new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON); + get_style_context().add_class("flat"); + Builder builder = new Builder.from_resource("/im/dino/menu_encryption.ui"); - PopoverMenu menu = builder.get_object("menu_encryption") as PopoverMenu; + popover = builder.get_object("menu_encryption") as PopoverMenu; Box encryption_box = builder.get_object("encryption_box") as Box; button_unencrypted = builder.get_object("button_unencrypted") as RadioButton; button_unencrypted.toggled.connect(encryption_changed); + Application app = GLib.Application.get_default() as Application; - foreach(var e in app.plugin_registry.encryption_list_entries) { + foreach (var e in app.plugin_registry.encryption_list_entries) { RadioButton btn = new RadioButton.with_label(button_unencrypted.get_group(), e.name); encryption_radios[btn] = e; btn.toggled.connect(encryption_changed); @@ -38,9 +32,6 @@ class EncryptionWidget : MenuButton, Plugins.ConversationTitlebarWidget { encryption_box.pack_end(btn, false); } clicked.connect(update_encryption_menu_state); - set_use_popover(true); - set_popover(menu); - set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON)); } private void encryption_changed() { @@ -66,13 +57,10 @@ class EncryptionWidget : MenuButton, Plugins.ConversationTitlebarWidget { } private void update_encryption_menu_icon() { - visible = (conversation.type_ == Conversation.Type.CHAT); - if (conversation.type_ == Conversation.Type.CHAT) { - if (conversation.encryption == Encryption.NONE) { - set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON)); - } else { - set_image(new Image.from_icon_name("changes-prevent-symbolic", IconSize.BUTTON)); - } + if (conversation.encryption == Encryption.NONE) { + set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON)); + } else { + set_image(new Image.from_icon_name("changes-prevent-symbolic", IconSize.BUTTON)); } } diff --git a/main/src/ui/chat_input/view.vala b/main/src/ui/chat_input/view.vala index 06e59e54..ef749505 100644 --- a/main/src/ui/chat_input/view.vala +++ b/main/src/ui/chat_input/view.vala @@ -12,6 +12,7 @@ public class View : Box { [GtkChild] private ScrolledWindow scrolled; [GtkChild] private TextView text_input; + [GtkChild] private Box box; public string text { owned get { return text_input.buffer.text; } @@ -25,6 +26,7 @@ public class View : Box { private OccupantsTabCompletor occupants_tab_completor; private SmileyConverter smiley_converter; private EditHistory edit_history; + private EncryptionButton encryption_widget = new EncryptionButton() { yalign=0, visible=true }; public View(StreamInteractor stream_interactor) { this.stream_interactor = stream_interactor; @@ -32,6 +34,8 @@ public class View : Box { smiley_converter = new SmileyConverter(stream_interactor, text_input); edit_history = new EditHistory(text_input, GLib.Application.get_default()); + box.add(encryption_widget); + encryption_widget.get_style_context().add_class("dino-chatinput-button"); scrolled.get_vscrollbar().get_preferred_height(out vscrollbar_min_height, null); scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify); text_input.key_press_event.connect(on_text_input_key_press); @@ -41,6 +45,7 @@ public class View : Box { public void initialize_for_conversation(Conversation conversation) { occupants_tab_completor.initialize_for_conversation(conversation); edit_history.initialize_for_conversation(conversation); + encryption_widget.set_conversation(conversation); if (this.conversation != null) entry_cache[this.conversation] = text_input.buffer.text; this.conversation = conversation; diff --git a/main/src/ui/conversation_summary/conversation_item_skeleton.vala b/main/src/ui/conversation_summary/conversation_item_skeleton.vala index 1eb76840..03114227 100644 --- a/main/src/ui/conversation_summary/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_summary/conversation_item_skeleton.vala @@ -39,7 +39,7 @@ public class ConversationItemSkeleton : Grid { } else { set_title_widget(widget); } - item.notify["mark"].connect_after(update_received); + item.notify["mark"].connect_after(() => { Idle.add(() => { update_received(); return false; }); }); update_received(); } diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala index 3f5a85b6..dcd24652 100644 --- a/main/src/ui/conversation_summary/conversation_view.vala +++ b/main/src/ui/conversation_summary/conversation_view.vala @@ -98,7 +98,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection { item.display_time.difference(lower_start_item.display_time) < TimeSpan.MINUTE && lower_start_item.jid.equals(item.jid) && lower_start_item.encryption == item.encryption && - item.mark != Message.Marked.WONTSEND) { + (item.mark == Message.Marked.WONTSEND) == (lower_start_item.mark == Message.Marked.WONTSEND)) { lower_skeleton.add_meta_item(item); force_alloc_width(lower_skeleton, main.get_allocated_width()); item_item_skeletons[item] = lower_skeleton; diff --git a/main/src/ui/conversation_titlebar/view.vala b/main/src/ui/conversation_titlebar/view.vala index 9949f4fc..32d829fb 100644 --- a/main/src/ui/conversation_titlebar/view.vala +++ b/main/src/ui/conversation_titlebar/view.vala @@ -22,7 +22,6 @@ public class ConversationTitlebar : Gtk.HeaderBar { Application app = GLib.Application.get_default() as Application; app.plugin_registry.register_contact_titlebar_entry(new MenuEntry(stream_interactor)); - app.plugin_registry.register_contact_titlebar_entry(new EncryptionEntry()); app.plugin_registry.register_contact_titlebar_entry(new OccupantsEntry(stream_interactor, window)); foreach(var e in app.plugin_registry.conversation_titlebar_entries) { @@ -33,6 +32,7 @@ public class ConversationTitlebar : Gtk.HeaderBar { } } + stream_interactor.get_module(MucManager.IDENTITY).subject_set.connect((account, jid, subject) => { Idle.add(() => { if (conversation != null && conversation.counterpart.equals_bare(jid) && conversation.account.equals(account)) { diff --git a/main/src/ui/notifications.vala b/main/src/ui/notifications.vala index 02f0b1de..470f9d6d 100644 --- a/main/src/ui/notifications.vala +++ b/main/src/ui/notifications.vala @@ -63,6 +63,7 @@ public class Notifications : Object { try { notifications[conversation].show(); } catch (Error error) { } + window.urgency_hint = true; } } diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala index b3c02189..69b530d7 100644 --- a/main/src/ui/unified_window.vala +++ b/main/src/ui/unified_window.vala @@ -126,6 +126,7 @@ public class UnifiedWindow : Window { private bool on_focus_in_event() { stream_interactor.get_module(ChatInteraction.IDENTITY).on_window_focus_in(conversation); + urgency_hint = false; return false; } diff --git a/plugins/openpgp/src/database.vala b/plugins/openpgp/src/database.vala index 7d41db3c..0e4bf74c 100644 --- a/plugins/openpgp/src/database.vala +++ b/plugins/openpgp/src/database.vala @@ -46,7 +46,7 @@ public class Database : Qlite.Database { public string? get_contact_key(Jid jid) { return contact_key_table.select({contact_key_table.key}) - .with(contact_key_table.jid, "=", jid.bare_jid.to_string())[contact_key_table.key]; + .with(contact_key_table.jid, "=", jid.to_string())[contact_key_table.key]; } public void set_account_key(Account account, string key) { @@ -64,4 +64,4 @@ public class Database : Qlite.Database { public override void migrate(long oldVersion) { } } -} \ No newline at end of file +} diff --git a/plugins/openpgp/src/encryption_list_entry.vala b/plugins/openpgp/src/encryption_list_entry.vala index 584e065b..e0a11865 100644 --- a/plugins/openpgp/src/encryption_list_entry.vala +++ b/plugins/openpgp/src/encryption_list_entry.vala @@ -1,3 +1,5 @@ +using Gee; + using Dino.Entities; namespace Dino.Plugins.OpenPgp { @@ -19,8 +21,23 @@ private class EncryptionListEntry : Plugins.EncryptionListEntry, Object { }} public bool can_encrypt(Entities.Conversation conversation) { - string? key_id = stream_interactor.get_module(Manager.IDENTITY).get_key_id(conversation.account, conversation.counterpart); - return key_id != null && GPGHelper.get_keylist(key_id).size > 0; + if (conversation.type_ == Conversation.Type.CHAT) { + string? key_id = stream_interactor.get_module(Manager.IDENTITY).get_key_id(conversation.account, conversation.counterpart); + return key_id != null && GPGHelper.get_keylist(key_id).size > 0; + } else if (conversation.type_ == Conversation.Type.GROUPCHAT) { + Gee.List muc_jids = new Gee.ArrayList(); + Gee.List? occupants = stream_interactor.get_module(MucManager.IDENTITY).get_occupants(conversation.counterpart, conversation.account); + if (occupants != null) muc_jids.add_all(occupants); + Gee.List? offline_members = stream_interactor.get_module(MucManager.IDENTITY).get_offline_members(conversation.counterpart, conversation.account); + if (occupants != null) muc_jids.add_all(offline_members); + + foreach (Jid jid in muc_jids) { + string? key_id = stream_interactor.get_module(Manager.IDENTITY).get_key_id(conversation.account, jid); + if (key_id == null) return false; + } + return true; + } + return false; } } diff --git a/plugins/openpgp/src/manager.vala b/plugins/openpgp/src/manager.vala index 0a145283..4c8b6d13 100644 --- a/plugins/openpgp/src/manager.vala +++ b/plugins/openpgp/src/manager.vala @@ -6,69 +6,99 @@ using Dino.Entities; namespace Dino.Plugins.OpenPgp { - public class Manager : StreamInteractionModule, Object { - public static ModuleIdentity IDENTITY = new ModuleIdentity("pgp_manager"); - public string id { get { return IDENTITY.id; } } +public class Manager : StreamInteractionModule, Object { + public static ModuleIdentity IDENTITY = new ModuleIdentity("pgp_manager"); + public string id { get { return IDENTITY.id; } } - public const string MESSAGE_ENCRYPTED = "pgp"; + public const string MESSAGE_ENCRYPTED = "pgp"; - private StreamInteractor stream_interactor; - private Database db; - private HashMap pgp_key_ids = new HashMap(Jid.hash_bare_func, Jid.equals_bare_func); + private StreamInteractor stream_interactor; + private Database db; + private HashMap pgp_key_ids = new HashMap(Jid.hash_bare_func, Jid.equals_bare_func); - public static void start(StreamInteractor stream_interactor, Database db) { - Manager m = new Manager(stream_interactor, db); - stream_interactor.add_module(m); - } + public static void start(StreamInteractor stream_interactor, Database db) { + Manager m = new Manager(stream_interactor, db); + stream_interactor.add_module(m); + } - private Manager(StreamInteractor stream_interactor, Database db) { - this.stream_interactor = stream_interactor; - this.db = db; + private Manager(StreamInteractor stream_interactor, Database db) { + this.stream_interactor = stream_interactor; + this.db = db; - stream_interactor.account_added.connect(on_account_added); - stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_received.connect(on_pre_message_received); - stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(on_pre_message_send); - } + stream_interactor.account_added.connect(on_account_added); + stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_received.connect(on_pre_message_received); + stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(check_encypt); + } - private void on_pre_message_received(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) { - if (MessageFlag.get_flag(message_stanza) != null && MessageFlag.get_flag(message_stanza).decrypted) { - message.encryption = Encryption.PGP; - } - } - - private void on_pre_message_send(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) { - if (message.encryption == Encryption.PGP) { - string? key_id = get_key_id(conversation.account, message.counterpart); - bool encrypted = false; - if (key_id != null) { - Core.XmppStream? stream = stream_interactor.get_stream(conversation.account); - if (stream != null) encrypted = stream.get_module(Module.IDENTITY).encrypt(message_stanza, key_id); - } - if (!encrypted) { - message.marked = Entities.Message.Marked.WONTSEND; - } - } - } - - public string? get_key_id(Account account, Jid jid) { - return db.get_contact_key(jid); - } - - private void on_account_added(Account account) { - stream_interactor.module_manager.get_module(account, Module.IDENTITY).received_jid_key_id.connect((stream, jid, key_id) => { - on_jid_key_received(account, new Jid(jid), key_id); - }); - } - - private void on_jid_key_received(Account account, Jid jid, string key_id) { - lock (pgp_key_ids) { - if (!pgp_key_ids.has_key(jid) || pgp_key_ids[jid] != key_id) { - if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid, account)) { - db.set_contact_key(jid.bare_jid, key_id); - } - } - pgp_key_ids[jid] = key_id; - } + private void on_pre_message_received(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) { + if (MessageFlag.get_flag(message_stanza) != null && MessageFlag.get_flag(message_stanza).decrypted) { + message.encryption = Encryption.PGP; } } -} \ No newline at end of file + + private void check_encypt(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) { + if (message.encryption == Encryption.PGP) { + bool encrypted = false; + if (conversation.type_ == Conversation.Type.CHAT) { + encrypted = encrypt_for_chat(message, message_stanza, conversation); + } else if (conversation.type_ == Conversation.Type.GROUPCHAT) { + encrypted = encrypt_for_groupchat(message, message_stanza, conversation); + } + if (!encrypted) message.marked = Entities.Message.Marked.WONTSEND; + } + } + + private bool encrypt_for_chat(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) { + Core.XmppStream? stream = stream_interactor.get_stream(conversation.account); + if (stream == null) return false; + + string? key_id = get_key_id(conversation.account, message.counterpart); + if (key_id != null) { + return stream.get_module(Module.IDENTITY).encrypt(message_stanza, new Gee.ArrayList.wrap(new string[]{key_id})); + } + return false; + } + + private bool encrypt_for_groupchat(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) { + Core.XmppStream? stream = stream_interactor.get_stream(conversation.account); + if (stream == null) return false; + + Gee.List muc_jids = new Gee.ArrayList(); + Gee.List? occupants = stream_interactor.get_module(MucManager.IDENTITY).get_occupants(conversation.counterpart, conversation.account); + if (occupants != null) muc_jids.add_all(occupants); + Gee.List? offline_members = stream_interactor.get_module(MucManager.IDENTITY).get_offline_members(conversation.counterpart, conversation.account); + if (occupants != null) muc_jids.add_all(offline_members); + + Gee.List keys = new Gee.ArrayList(); + foreach (Jid jid in muc_jids) { + string? key_id = stream_interactor.get_module(Manager.IDENTITY).get_key_id(conversation.account, jid); + if (key_id != null && GPGHelper.get_keylist(key_id).size > 0 && !keys.contains(key_id)) { + keys.add(key_id); + } + } + return stream.get_module(Module.IDENTITY).encrypt(message_stanza, keys); + } + + public string? get_key_id(Account account, Jid jid) { + Jid search_jid = stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid, account) ? jid : jid.bare_jid; + return db.get_contact_key(search_jid); + } + + private void on_account_added(Account account) { + stream_interactor.module_manager.get_module(account, Module.IDENTITY).received_jid_key_id.connect((stream, jid, key_id) => { + on_jid_key_received(account, new Jid(jid), key_id); + }); + } + + private void on_jid_key_received(Account account, Jid jid, string key_id) { + lock (pgp_key_ids) { + if (!pgp_key_ids.has_key(jid) || pgp_key_ids[jid] != key_id) { + Jid set_jid = stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid, account) ? jid : jid.bare_jid; + db.set_contact_key(set_jid, key_id); + } + pgp_key_ids[jid] = key_id; + } + } +} + +} diff --git a/plugins/openpgp/src/stream_module.vala b/plugins/openpgp/src/stream_module.vala index 745f2005..6c55cdc5 100644 --- a/plugins/openpgp/src/stream_module.vala +++ b/plugins/openpgp/src/stream_module.vala @@ -33,8 +33,11 @@ namespace Dino.Plugins.OpenPgp { } } - public bool encrypt(Message.Stanza message, string key_id) { - string? enc_body = gpg_encrypt(message.body, new string[] {key_id, own_key.fpr}); + public bool encrypt(Message.Stanza message, Gee.List fprs) { + string[] encrypt_to = new string[fprs.size + 1]; + for (int i = 0; i < fprs.size; i++) encrypt_to[i] = fprs[i]; + encrypt_to[encrypt_to.length - 1] = own_key.fpr; + string? enc_body = gpg_encrypt(message.body, encrypt_to); if (enc_body != null) { message.stanza.put_node(new StanzaNode.build("x", NS_URI_ENCRYPTED).add_self_xmlns().put_node(new StanzaNode.text(enc_body))); message.body = "[This message is OpenPGP encrypted (see XEP-0027)]";