diff --git a/gajim/data/gui/start_chat_dialog.ui b/gajim/data/gui/start_chat_dialog.ui index 9857be9b4c2afa5b543b4fcbeca311995c085a83..a8db15485bf59455129ff42a2285f72d5e2ed126 100644 --- a/gajim/data/gui/start_chat_dialog.ui +++ b/gajim/data/gui/start_chat_dialog.ui @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.1 --> +<!-- Generated with glade 3.22.1 --> <interface> <requires lib="gtk+" version="3.20"/> <object class="GtkBox" id="box"> @@ -25,7 +25,7 @@ </packing> </child> <child> - <object class="GtkScrolledWindow"> + <object class="GtkScrolledWindow" id="scrolledwindow"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="hscrollbar_policy">never</property> @@ -53,4 +53,218 @@ </packing> </child> </object> + <object class="GtkGrid" id="search_result_grid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel" id="name_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="ellipsize">end</property> + <property name="max_width_chars">50</property> + <property name="xalign">0</property> + <style> + <class name="bold"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="jid_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="ellipsize">end</property> + <property name="max_width_chars">50</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + <property name="width">2</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="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">system-users-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="user_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Users</property> + <property name="xalign">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + </object> + <object class="GtkGrid" id="tooltip"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="column_spacing">6</property> + <child> + <object class="GtkLabel" id="tooltip_name"> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="halign">start</property> + <property name="ellipsize">end</property> + <property name="max_width_chars">50</property> + <property name="xalign">0</property> + <style> + <class name="bold"/> + </style> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="tooltip_jid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="ellipsize">end</property> + <property name="max_width_chars">50</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="tooltip_description"> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="halign">start</property> + <property name="wrap">True</property> + <property name="max_width_chars">65</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="tooltip_name_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Name:</property> + <property name="xalign">1</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="tooltip_description_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Description:</property> + <property name="xalign">1</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Address:</property> + <property name="xalign">1</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="tooltip_user"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">end</property> + <property name="valign">center</property> + <property name="icon_name">system-users-symbolic</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="anonymous_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Semi-Anonymous +<span size="small">Only moderators can see your XMPP-Address)</span></property> + <property name="use_markup">True</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkImage" id="anonymous_icon"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">end</property> + <property name="valign">center</property> + <property name="icon_name">security-medium-symbolic</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + </object> </interface> diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css index c45cb17119e35164b6fd2a6be152fca35b8841a7..fdc9caee39c69c2ad8ffa53b0fbf85d54e6d1f63 100644 --- a/gajim/data/style/gajim.css +++ b/gajim/data/style/gajim.css @@ -134,6 +134,15 @@ list.settings > row > box { #StartChatListBox > row { padding: 10px 20px 10px 10px; } #StartChatListBox > row:not(.activatable) label { color: @insensitive_fg_color } +/* StartChatListBox */ +.start-chat-row { border-bottom: 1px solid; border-color: @theme_unfocused_bg_color; } +.start-chat-row:last-child { border-bottom: 0px} +.start-chat-row.activatable:active { box-shadow: none; } +.start-chat-row { padding: 10px 20px 10px 10px; } +.start-chat-row:not(.activatable) label { color: @insensitive_fg_color } +.start-chat-row:focus { outline: none; } + + /* GroupchatConfig */ #GroupchatConfig > box > buttonbox { margin: 0px 12px 12px 12px; } #GroupchatConfig stack { border-bottom: 1px solid; border-color: @borders;} diff --git a/gajim/gtk/start_chat.py b/gajim/gtk/start_chat.py index 49a4aaff95ba2e12139ba5e7e82f31b6a8a95356..d3b56f9e2837547a40e2f35b12ff2d80416a4665 100644 --- a/gajim/gtk/start_chat.py +++ b/gajim/gtk/start_chat.py @@ -13,12 +13,16 @@ # along with Gajim. If not, see <http://www.gnu.org/licenses/>. import locale +from enum import IntEnum from gi.repository import Gdk from gi.repository import Gtk from gi.repository import GLib from gi.repository import Pango +from nbxmpp.util import is_error_result +from nbxmpp.const import AnonymityMode + from gajim.common import app from gajim.common import helpers from gajim.common.i18n import _ @@ -26,6 +30,15 @@ from gajim.common.const import AvatarSize from gajim.gtk.util import get_icon_name from gajim.gtk.util import get_builder +from gajim.gtk.util import ensure_not_destroyed + + +SERVICE_JID = 'rodrigo.de.mucobedo@dreckshal.de' + + +class Search(IntEnum): + CONTACT = 0 + MUC = 1 class StartChatDialog(Gtk.ApplicationWindow): @@ -38,6 +51,9 @@ class StartChatDialog(Gtk.ApplicationWindow): self.set_title(_('Start new Conversation')) self.set_default_size(-1, 400) self.ready_to_destroy = False + self._parameter_form = None + self._destroyed = False + self._search_stopped = False self.builder = get_builder('start_chat_dialog.ui') self.listbox = self.builder.get_object('listbox') @@ -66,6 +82,9 @@ class StartChatDialog(Gtk.ApplicationWindow): self.listbox.set_sort_func(self._sort_func, None) self.listbox.connect('row-activated', self._on_row_activated) + self._muc_search_listbox = MUCSearch() + self._current_listbox = self.listbox + self.connect('key-press-event', self._on_key_press) self.connect('destroy', self._destroy) @@ -118,7 +137,7 @@ class StartChatDialog(Gtk.ApplicationWindow): return True if (event.state == Gdk.ModifierType.SHIFT_MASK and - event.keyval == Gdk.KEY_ISO_Left_Tab): + event.keyval == Gdk.KEY_ISO_Left_Tab): self.search_entry.emit('previous-match') return True @@ -127,6 +146,8 @@ class StartChatDialog(Gtk.ApplicationWindow): return True if event.keyval == Gdk.KEY_Escape: + self._search_stopped = True + self._muc_search_listbox.remove_all() if self.search_entry.get_text() != '': self.search_entry.emit('stop-search') else: @@ -134,6 +155,11 @@ class StartChatDialog(Gtk.ApplicationWindow): return True if event.keyval == Gdk.KEY_Return: + if self._current_listbox_is(Search.MUC): + self._muc_search_listbox.remove_all() + self._start_search() + return True + row = self.listbox.get_selected_row() if row is not None: row.emit('activate') @@ -159,8 +185,27 @@ class StartChatDialog(Gtk.ApplicationWindow): self.ready_to_destroy = True + def _set_listbox(self, listbox): + if self._current_listbox == listbox: + return + viewport = self.builder.scrolledwindow.get_child() + viewport.remove(viewport.get_child()) + self.builder.scrolledwindow.remove(viewport) + self.builder.scrolledwindow.add(listbox) + self._current_listbox = listbox + + def _current_listbox_is(self, box): + if self._current_listbox == self.listbox: + return box == Search.CONTACT + return box == Search.MUC + def _on_search_changed(self, entry): search_text = entry.get_text() + if search_text.startswith('g:'): + self._set_listbox(self._muc_search_listbox) + return + + self._set_listbox(self.listbox) if '@' in search_text: self._add_new_jid_row() self._update_new_jid_rows(search_text) @@ -256,7 +301,83 @@ class StartChatDialog(Gtk.ApplicationWindow): return locale.strcoll(name1.lower(), name2.lower()) @staticmethod - def _destroy(*args): + def _get_connection(): + accounts = app.get_connected_accounts() + if not accounts: + raise NotConnected + return app.connections[accounts.pop()].connection + + def _start_search(self): + self._search_stopped = False + try: + con = self._get_connection() + except NotConnected: + return + + text = self.search_entry.get_text() + text = text[2:].strip() + self._muc_search_listbox.start_search() + + if self._parameter_form is None: + con.get_module('Muclumbus').request_parameters( + SERVICE_JID, + callback=self._parameters_received, + user_data=(con, text)) + else: + self._parameter_form.vars['q'].value = text + + con.get_module('Muclumbus').set_search( + SERVICE_JID, + self._parameter_form, + callback=self._on_search_result, + user_data=con) + + @ensure_not_destroyed + def _parameters_received(self, result, user_data): + if is_error_result(result): + self._on_error(result) + return + + con, text = user_data + self._parameter_form = result + self._parameter_form.type_ = 'submit' + self._parameter_form.vars['q'].value = text + + con.get_module('Muclumbus').set_search( + SERVICE_JID, + self._parameter_form, + callback=self._on_search_result, + user_data=con) + + @ensure_not_destroyed + def _on_search_result(self, result, con): + if self._search_stopped: + return + + if is_error_result(result): + self._on_error(result) + return + + for item in result.items: + self._muc_search_listbox.add(ResultRow(item)) + + if result.end: + self._muc_search_listbox.end_search() + return + + con.get_module('Muclumbus').set_search( + SERVICE_JID, + self._parameter_form, + items_per_page=result.max, + after=result.last, + callback=self._on_search_result, + user_data=con) + + def _on_error(self, result): + self._muc_search_listbox.show_error(result) + + def _destroy(self, *args): + self._destroyed = True del app.interface.instances['start_chat'] @@ -345,3 +466,149 @@ class ContactRow(Gtk.Grid): if self.show_account: return '%s %s %s' % (self.name, self.jid, self.account_label) return '%s %s' % (self.name, self.jid) + + +class MUCSearch(Gtk.ListBox): + def __init__(self): + Gtk.ListBox.__init__(self) + self.set_has_tooltip(True) + self.set_activate_on_single_click(False) + self._progress = None + + self._ui = get_builder('start_chat_dialog.ui', ['tooltip']) + self.connect('query-tooltip', self._query_tooltip) + self.connect('row-activated', self._on_row_activated) + self.show_all() + + def remove_all(self): + def remove(row): + self.remove(row) + row.destroy() + self.foreach(remove) + + def show_error(self, error): + self.insert(ErrorRow(error), 0) + self.remove(self._progress) + self._progress.destroy() + + def start_search(self): + self._progress = ProgressRow() + super().add(self._progress) + + def end_search(self): + self._progress.stop() + + def add(self, row): + super().add(row) + self._progress.update() + + def _query_tooltip(self, _widget, _x_pos, y_pos, _keyboard_mode, tooltip): + row = self.get_row_at_y(y_pos) + if row is None or not isinstance(row, ResultRow): + self._clear_tooltip() + return False + + if row.item.anonymity_mode == AnonymityMode.SEMI: + self._ui.anonymous_icon.show() + self._ui.anonymous_label.show() + + self._ui.tooltip_user.set_text(row.item.nusers) + self._ui.tooltip_jid.set_text(row.item.jid) + if row.item.description: + self._ui.tooltip_description.set_text(row.item.description) + self._ui.tooltip_description.show() + self._ui.tooltip_description_label.show() + + if row.item.name: + self._ui.tooltip_name.set_text(row.item.name) + self._ui.tooltip_name.show() + self._ui.tooltip_name_label.show() + + tooltip.set_custom(self._ui.tooltip) + return True + + def _clear_tooltip(self): + self._ui.anonymous_icon.hide() + self._ui.anonymous_label.hide() + self._ui.tooltip_description.hide() + self._ui.tooltip_description_label.hide() + self._ui.tooltip_name.hide() + self._ui.tooltip_name_label.hide() + + def _on_row_activated(self, _listbox, row): + app.interface.join_gc_minimal(None, row.item.jid) + + +class ResultRow(Gtk.ListBoxRow): + def __init__(self, item): + Gtk.ListBoxRow.__init__(self) + self.set_activatable(True) + self.get_style_context().add_class('start-chat-row') + self.item = item + self._ui = get_builder('start_chat_dialog.ui', ['search_result_grid']) + self._ui.jid_label.set_text(item.jid) + self._ui.name_label.set_text(item.name) + self._ui.user_label.set_text(item.nusers) + self.add(self._ui.search_result_grid) + self.show_all() + + +class ErrorRow(Gtk.ListBoxRow): + def __init__(self, error): + Gtk.ListBoxRow.__init__(self) + self.set_selectable(False) + self.set_activatable(False) + self.get_style_context().add_class('start-chat-row') + self._error = Gtk.Label(label=str(error)) + self._error.set_line_wrap(True) + self._error.set_line_wrap_mode(Pango.WrapMode.WORD) + self._error.set_xalign(0) + self._error_image = Gtk.Image.new_from_icon_name( + 'dialog-error', Gtk.IconSize.MENU) + self._error_image.get_style_context().add_class('error-color') + self._error_image.set_valign(Gtk.Align.CENTER) + box = Gtk.Box() + box.set_spacing(6) + box.add(self._error_image) + box.add(self._error) + self.add(box) + self.show_all() + + +class ProgressRow(Gtk.ListBoxRow): + def __init__(self): + Gtk.ListBoxRow.__init__(self) + self.set_selectable(False) + self.set_activatable(False) + self.get_style_context().add_class('start-chat-row') + self._text = _('%s Group chats found') + self._count = 0 + self._spinner = Gtk.Spinner() + self._spinner.start() + self._count_label = Gtk.Label(label=self._text % 0) + self._count_label.get_style_context().add_class('bold') + self._finished_image = Gtk.Image.new_from_icon_name( + 'emblem-ok-symbolic', Gtk.IconSize.MENU) + self._finished_image.get_style_context().add_class('success-color') + self._finished_image.set_no_show_all(True) + + box = Gtk.Box() + box.set_spacing(6) + box.add(self._finished_image) + box.add(self._spinner) + box.add(self._count_label) + self.add(box) + self.show_all() + + def update(self): + self._count += 1 + self._count_label.set_text(self._text % self._count) + + def stop(self): + self._spinner.stop() + self._spinner.hide() + self._finished_image.show() + + +class NotConnected(Exception): + pass