start_chat.py 28 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.

import locale
16
from enum import IntEnum
17
from functools import partial
18 19 20 21 22 23

from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Pango

24 25
from nbxmpp.util import is_error_result

26
from gajim.common import app
27
from gajim.common.helpers import validate_jid
28
from gajim.common.helpers import to_user_string
29
from gajim.common.helpers import get_groupchat_name
30
from gajim.common.helpers import get_alternative_venue
31
from gajim.common.i18n import _
32
from gajim.common.i18n import get_rfc5646_lang
33
from gajim.common.const import AvatarSize
34
from gajim.common.const import MUC_DISCO_ERRORS
35

36
from gajim.gtk.groupchat_info import GroupChatInfoScrolled
37
from gajim.gtk.util import get_builder
38
from gajim.gtk.util import ensure_not_destroyed
39
from gajim.gtk.util import get_icon_name
40 41 42 43


class Search(IntEnum):
    CONTACT = 0
44
    GLOBAL = 1
45 46 47 48 49 50 51 52 53


class StartChatDialog(Gtk.ApplicationWindow):
    def __init__(self):
        Gtk.ApplicationWindow.__init__(self)
        self.set_name('StartChatDialog')
        self.set_application(app.app)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_show_menubar(False)
54
        self.set_title(_('Start New Conversation'))
55 56
        self.set_default_size(-1, 400)
        self.ready_to_destroy = False
57
        self._parameter_form = None
58
        self._keywords = []
59 60
        self._destroyed = False
        self._search_stopped = False
61
        self._redirected = False
62
        self._source_id = None
63

Daniel Brötzmann's avatar
Daniel Brötzmann committed
64
        self._ui = get_builder('start_chat_dialog.ui')
65
        self.add(self._ui.stack)
66 67 68 69

        self.new_contact_row_visible = False
        self.new_contact_rows = {}
        self.new_groupchat_rows = {}
70
        self._accounts = app.get_enabled_accounts_with_labels()
71 72

        rows = []
73
        self._add_accounts()
74 75
        self._add_contacts(rows)
        self._add_groupchats(rows)
76

Daniel Brötzmann's avatar
Daniel Brötzmann committed
77 78 79 80 81 82 83 84
        self._ui.search_entry.connect('search-changed',
                                      self._on_search_changed)
        self._ui.search_entry.connect('next-match',
                                      self._select_new_match, 'next')
        self._ui.search_entry.connect('previous-match',
                                      self._select_new_match, 'prev')
        self._ui.search_entry.connect(
            'stop-search', lambda *args: self._ui.search_entry.set_text(''))
85

86
        self._ui.listbox.set_placeholder(self._ui.placeholder)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
87 88
        self._ui.listbox.set_filter_func(self._filter_func, None)
        self._ui.listbox.connect('row-activated', self._on_row_activated)
89

90 91 92
        self._global_search_listbox = GlobalSearch()
        self._global_search_listbox.connect('row-activated',
                                            self._on_row_activated)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
93
        self._current_listbox = self._ui.listbox
94

95 96 97
        self._muc_info_box = GroupChatInfoScrolled()
        self._ui.info_box.add(self._muc_info_box)

98 99 100 101
        self.connect('key-press-event', self._on_key_press)
        self.connect('destroy', self._destroy)

        self.select_first_row()
102
        self._ui.connect_signals(self)
103 104
        self.show_all()

105 106 107
        if rows:
            self._load_contacts(rows)

108 109 110
    def set_search_text(self, text):
        self._ui.search_entry.set_text(text)

111 112 113 114 115 116 117
    def _global_search_active(self):
        return self._ui.global_search_toggle.get_active()

    def _add_accounts(self):
        for account in self._accounts:
            self._ui.account_store.append([None, *account])

118
    def _add_contacts(self, rows):
119 120
        show_account = len(self._accounts) > 1
        for account, _label in self._accounts:
121 122 123 124
            self.new_contact_rows[account] = None
            for jid in app.contacts.get_jid_list(account):
                contact = app.contacts.get_contact_with_highest_priority(
                    account, jid)
Philipp Hörist's avatar
Philipp Hörist committed
125
                if contact.is_groupchat:
126
                    continue
127 128
                rows.append(ContactRow(account, contact, jid,
                                       contact.get_shown_name(), show_account))
129

130
    def _add_groupchats(self, rows):
131 132
        show_account = len(self._accounts) > 1
        for account, _label in self._accounts:
133 134 135
            self.new_groupchat_rows[account] = None
            con = app.connections[account]
            bookmarks = con.get_module('Bookmarks').bookmarks
Philipp Hörist's avatar
Philipp Hörist committed
136
            for bookmark in bookmarks:
137 138
                jid = str(bookmark.jid)
                name = get_groupchat_name(con, jid)
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
                rows.append(ContactRow(account, None, jid,
                                       name, show_account, True))

    def _load_contacts(self, rows):
        generator = self._incremental_add(rows)
        self._source_id = GLib.idle_add(lambda: next(generator, False),
                                        priority=GLib.PRIORITY_LOW)

    def _incremental_add(self, rows):
        for row in rows:
            self._ui.listbox.add(row)
            yield True

        self._ui.listbox.set_sort_func(self._sort_func, None)
        self._source_id = None
154

155 156 157 158
    def _on_page_changed(self, stack, _param):
        if stack.get_visible_child_name() == 'account':
            self._ui.account_view.grab_focus()

159
    def _on_row_activated(self, _listbox, row):
160 161 162 163 164 165 166 167 168 169
        if self._current_listbox_is(Search.GLOBAL):
            self._select_muc()
        else:
            self._start_new_chat(row)

    def _select_muc(self):
        if len(self._accounts) > 1:
            self._ui.stack.set_visible_child_name('account')
        else:
            self._on_select_clicked()
170

171
    def _on_key_press(self, _widget, event):
172
        is_search = self._ui.stack.get_visible_child_name() == 'search'
173
        if event.keyval in (Gdk.KEY_Down, Gdk.KEY_Tab):
174 175 176 177 178 179
            if not is_search:
                return Gdk.EVENT_PROPAGATE

            if self._global_search_active():
                self._global_search_listbox.select_next()
            else:
180
                self._ui.search_entry.emit('next-match')
181
            return Gdk.EVENT_STOP
182 183

        if (event.state == Gdk.ModifierType.SHIFT_MASK and
184
                event.keyval == Gdk.KEY_ISO_Left_Tab):
185 186 187 188 189 190
            if not is_search:
                return Gdk.EVENT_PROPAGATE

            if self._global_search_active():
                self._global_search_listbox.select_prev()
            else:
191
                self._ui.search_entry.emit('previous-match')
192
            return Gdk.EVENT_STOP
193 194

        if event.keyval == Gdk.KEY_Up:
195 196 197 198 199 200
            if not is_search:
                return Gdk.EVENT_PROPAGATE

            if self._global_search_active():
                self._global_search_listbox.select_prev()
            else:
201
                self._ui.search_entry.emit('previous-match')
202
            return Gdk.EVENT_STOP
203 204

        if event.keyval == Gdk.KEY_Escape:
205 206 207 208
            if self._ui.stack.get_visible_child_name() == 'progress':
                self.destroy()
                return Gdk.EVENT_STOP

209 210 211 212
            if self._ui.stack.get_visible_child_name() == 'account':
                self._on_back_clicked()
                return Gdk.EVENT_STOP

213 214 215 216
            if self._ui.stack.get_visible_child_name() in ('error', 'info'):
                self._ui.stack.set_visible_child_name('search')
                return Gdk.EVENT_STOP

217
            self._search_stopped = True
218 219 220
            self._ui.search_entry.grab_focus()
            self._scroll_to_first_row()
            self._global_search_listbox.remove_all()
Daniel Brötzmann's avatar
Daniel Brötzmann committed
221 222
            if self._ui.search_entry.get_text() != '':
                self._ui.search_entry.emit('stop-search')
223 224
            else:
                self.destroy()
225
            return Gdk.EVENT_STOP
226 227

        if event.keyval == Gdk.KEY_Return:
228 229 230
            if self._ui.stack.get_visible_child_name() == 'progress':
                return Gdk.EVENT_STOP

231 232 233 234
            if self._ui.stack.get_visible_child_name() == 'account':
                self._on_select_clicked()
                return Gdk.EVENT_STOP

235 236 237 238 239 240 241 242
            if self._ui.stack.get_visible_child_name() == 'error':
                self._ui.stack.set_visible_child_name('search')
                return Gdk.EVENT_STOP

            if self._ui.stack.get_visible_child_name() == 'info':
                self._on_join_clicked()
                return Gdk.EVENT_STOP

243 244 245 246 247 248 249
            if self._current_listbox_is(Search.GLOBAL):
                if self._ui.search_entry.is_focus():
                    self._global_search_listbox.remove_all()
                    self._start_search()

                elif self._global_search_listbox.get_selected_row() is not None:
                    self._select_muc()
250
                return Gdk.EVENT_STOP
251

Daniel Brötzmann's avatar
Daniel Brötzmann committed
252
            row = self._ui.listbox.get_selected_row()
253 254
            if row is not None:
                row.emit('activate')
255
            return Gdk.EVENT_STOP
256

257 258 259
        if is_search:
            self._ui.search_entry.grab_focus_without_selecting()
        return Gdk.EVENT_PROPAGATE
260 261 262 263

    def _start_new_chat(self, row):
        if row.new:
            try:
264 265
                validate_jid(row.jid)
            except ValueError as error:
266
                self._show_error_page(error)
267 268 269
                return

        if row.groupchat:
270
            if not app.account_is_available(row.account):
271 272 273
                self._show_error_page(_('You can not join a group chat '
                                        'unless you are connected.'))
                return
274 275 276 277 278 279

            self.ready_to_destroy = True
            if app.interface.show_groupchat(row.account, row.jid):
                return

            self.ready_to_destroy = False
280
            self._redirected = False
281
            self._disco_muc(row.account, row.jid)
282

283 284
        else:
            app.interface.new_chat_from_jid(row.account, row.jid)
285
            self.ready_to_destroy = True
286

287 288 289
    def _disco_muc(self, account, jid):
        self._ui.stack.set_visible_child_name('progress')
        con = app.connections[account]
290 291
        con.get_module('Discovery').disco_muc(
            jid, callback=partial(self._disco_info_received, account))
292 293

    @ensure_not_destroyed
294
    def _disco_info_received(self, account, result):
295
        if is_error_result(result):
296 297 298 299 300 301 302
            jid = get_alternative_venue(result)
            if jid is None or self._redirected:
                self._set_error(result)
                return

            self._redirected = True
            self._disco_muc(account, jid)
303 304 305 306 307 308 309 310 311 312

        elif result.is_muc:
            self._muc_info_box.set_account(account)
            self._muc_info_box.set_from_disco_info(result)
            self._ui.stack.set_visible_child_name('info')

        else:
            self._set_error_from_code('not-muc-service')

    def _set_error(self, error):
313
        text = MUC_DISCO_ERRORS.get(error.condition, to_user_string(error))
314 315 316 317
        if error.condition == 'gone':
            reason = error.get_text(get_rfc5646_lang())
            if reason:
                text = '%s:\n%s' % (text, reason)
318 319 320 321 322 323
        self._show_error_page(text)

    def _set_error_from_code(self, error_code):
        self._show_error_page(MUC_DISCO_ERRORS[error_code])

    def _show_error_page(self, text):
324
        self._ui.error_label.set_text(str(text))
325 326 327
        self._ui.stack.set_visible_child_name('error')

    def _on_join_clicked(self, _button=None):
328 329
        account = self._muc_info_box.get_account()
        jid = self._muc_info_box.get_jid()
Philipp Hörist's avatar
Philipp Hörist committed
330
        app.interface.show_or_join_groupchat(account, str(jid))
331 332
        self.ready_to_destroy = True

333
    def _on_back_clicked(self, _button=None):
334 335
        self._ui.stack.set_visible_child_name('search')

336
    def _on_select_clicked(self, *args):
337 338 339 340 341 342 343 344 345 346 347 348
        model, iter_ = self._ui.account_view.get_selection().get_selected()
        if iter_ is not None:
            account = model[iter_][1]
        elif len(self._accounts) == 1:
            account = self._accounts[0][0]
        else:
            return

        selected_row = self._global_search_listbox.get_selected_row()
        if selected_row is None:
            return

349
        if not app.account_is_available(account):
350 351 352
            self._show_error_page(_('You can not join a group chat '
                                    'unless you are connected.'))
            return
353 354

        self._redirected = False
355 356
        self._disco_muc(account, selected_row.jid)

357 358 359
    def _set_listbox(self, listbox):
        if self._current_listbox == listbox:
            return
Daniel Brötzmann's avatar
Daniel Brötzmann committed
360
        viewport = self._ui.scrolledwindow.get_child()
361
        viewport.remove(viewport.get_child())
Daniel Brötzmann's avatar
Daniel Brötzmann committed
362 363
        self._ui.scrolledwindow.remove(viewport)
        self._ui.scrolledwindow.add(listbox)
364 365 366
        self._current_listbox = listbox

    def _current_listbox_is(self, box):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
367
        if self._current_listbox == self._ui.listbox:
368
            return box == Search.CONTACT
369 370 371 372 373
        return box == Search.GLOBAL

    def _on_global_search_toggle(self, button):
        self._ui.search_entry.set_text('')
        self._ui.search_entry.grab_focus()
374
        image_style_context = button.get_children()[0].get_style_context()
375
        if button.get_active():
376
            image_style_context.add_class('selected-color')
377 378 379 380
            self._set_listbox(self._global_search_listbox)
            self._remove_new_jid_row()
            self._ui.listbox.invalidate_filter()
        else:
381
            image_style_context.remove_class('selected-color')
382 383
            self._set_listbox(self._ui.listbox)
            self._global_search_listbox.remove_all()
384

385
    def _on_search_changed(self, entry):
386
        if self._global_search_active():
387 388
            return

389
        search_text = entry.get_text()
390 391 392 393 394
        if '@' in search_text:
            self._add_new_jid_row()
            self._update_new_jid_rows(search_text)
        else:
            self._remove_new_jid_row()
Daniel Brötzmann's avatar
Daniel Brötzmann committed
395
        self._ui.listbox.invalidate_filter()
396 397 398 399 400

    def _add_new_jid_row(self):
        if self.new_contact_row_visible:
            return
        for account in self.new_contact_rows:
401
            show_account = len(self._accounts) > 1
402 403 404 405
            row = ContactRow(account, None, '', None, show_account)
            self.new_contact_rows[account] = row
            group_row = ContactRow(account, None, '', None, show_account, True)
            self.new_groupchat_rows[account] = group_row
Daniel Brötzmann's avatar
Daniel Brötzmann committed
406 407
            self._ui.listbox.add(row)
            self._ui.listbox.add(group_row)
408 409 410 411 412 413 414
            row.get_parent().show_all()
        self.new_contact_row_visible = True

    def _remove_new_jid_row(self):
        if not self.new_contact_row_visible:
            return
        for account in self.new_contact_rows:
Daniel Brötzmann's avatar
Daniel Brötzmann committed
415
            self._ui.listbox.remove(
416
                self.new_contact_rows[account])
Daniel Brötzmann's avatar
Daniel Brötzmann committed
417
            self._ui.listbox.remove(
418
                self.new_groupchat_rows[account])
419 420 421 422 423 424 425
        self.new_contact_row_visible = False

    def _update_new_jid_rows(self, search_text):
        for account in self.new_contact_rows:
            self.new_contact_rows[account].update_jid(search_text)
            self.new_groupchat_rows[account].update_jid(search_text)

426
    def _select_new_match(self, _entry, direction):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
427
        selected_row = self._ui.listbox.get_selected_row()
428 429 430
        if selected_row is None:
            return

431 432 433 434 435 436 437 438
        index = selected_row.get_index()

        if direction == 'next':
            index += 1
        else:
            index -= 1

        while True:
Daniel Brötzmann's avatar
Daniel Brötzmann committed
439
            new_selected_row = self._ui.listbox.get_row_at_index(index)
440 441 442
            if new_selected_row is None:
                return
            if new_selected_row.get_child_visible():
Daniel Brötzmann's avatar
Daniel Brötzmann committed
443
                self._ui.listbox.select_row(new_selected_row)
444 445 446 447 448 449 450 451
                new_selected_row.grab_focus()
                return
            if direction == 'next':
                index += 1
            else:
                index -= 1

    def select_first_row(self):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
452 453
        first_row = self._ui.listbox.get_row_at_y(0)
        self._ui.listbox.select_row(first_row)
454

455 456 457
    def _scroll_to_first_row(self):
        self._ui.scrolledwindow.get_vadjustment().set_value(0)

458
    def _filter_func(self, row, _user_data):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
459
        search_text = self._ui.search_entry.get_text().lower()
460
        search_text_list = search_text.split()
461
        row_text = row.get_search_text().lower()
462 463 464
        for text in search_text_list:
            if text not in row_text:
                GLib.timeout_add(50, self.select_first_row)
465
                return None
466 467 468 469
        GLib.timeout_add(50, self.select_first_row)
        return True

    @staticmethod
470
    def _sort_func(row1, row2, _user_data):
471 472 473 474 475 476 477 478
        name1 = row1.get_search_text()
        name2 = row2.get_search_text()
        account1 = row1.account
        account2 = row2.account
        is_groupchat1 = row1.groupchat
        is_groupchat2 = row2.groupchat
        new1 = row1.new
        new2 = row2.new
479 480 481 482 483 484 485 486 487 488 489 490 491

        result = locale.strcoll(account1.lower(), account2.lower())
        if result != 0:
            return result

        if new1 != new2:
            return 1 if new1 else -1

        if is_groupchat1 != is_groupchat2:
            return 1 if is_groupchat1 else -1

        return locale.strcoll(name1.lower(), name2.lower())

492 493
    def _start_search(self):
        self._search_stopped = False
494 495
        accounts = app.get_connected_accounts()
        if not accounts:
496
            return
497
        con = app.connections[accounts[0]].connection
498

499 500
        text = self._ui.search_entry.get_text().strip()
        self._global_search_listbox.start_search()
501

502 503 504 505 506 507
        if app.config.get('muclumbus_api_pref') == 'http':
            self._start_http_search(con, text)
        else:
            self._start_iq_search(con, text)

    def _start_iq_search(self, con, text):
508 509
        if self._parameter_form is None:
            con.get_module('Muclumbus').request_parameters(
510
                app.config.get('muclumbus_api_jid'),
511 512 513 514 515 516
                callback=self._parameters_received,
                user_data=(con, text))
        else:
            self._parameter_form.vars['q'].value = text

            con.get_module('Muclumbus').set_search(
517
                app.config.get('muclumbus_api_jid'),
518 519
                self._parameter_form,
                callback=self._on_search_result,
520 521 522 523 524 525 526 527 528
                user_data=(con, False))

    def _start_http_search(self, con, text):
        self._keywords = text.split(' ')
        con.get_module('Muclumbus').set_http_search(
            app.config.get('muclumbus_api_http_uri'),
            self._keywords,
            callback=self._on_search_result,
            user_data=(con, True))
529 530 531 532

    @ensure_not_destroyed
    def _parameters_received(self, result, user_data):
        if is_error_result(result):
533
            self._global_search_listbox.remove_progress()
534
            self._show_error_page(to_user_string(result))
535 536 537 538 539
            return

        con, text = user_data
        self._parameter_form = result
        self._parameter_form.type_ = 'submit'
540
        self._start_iq_search(con, text)
541 542

    @ensure_not_destroyed
543
    def _on_search_result(self, result, user_data):
544 545 546 547
        if self._search_stopped:
            return

        if is_error_result(result):
548
            self._global_search_listbox.remove_progress()
549
            self._show_error_page(to_user_string(result))
550 551 552
            return

        for item in result.items:
553
            self._global_search_listbox.add(ResultRow(item))
554 555

        if result.end:
556
            self._global_search_listbox.end_search()
557 558
            return

559 560 561 562 563 564 565
        con, http = user_data
        if http:
            self._continue_http_search(result, con)
        else:
            self._continue_iq_search(result, con)

    def _continue_iq_search(self, result, con):
566
        con.get_module('Muclumbus').set_search(
567
            app.config.get('muclumbus_api_jid'),
568 569 570 571
            self._parameter_form,
            items_per_page=result.max,
            after=result.last,
            callback=self._on_search_result,
572
            user_data=(con, False))
573

574 575 576 577 578 579
    def _continue_http_search(self, result, con):
        con.get_module('Muclumbus').set_http_search(
            app.config.get('muclumbus_api_http_uri'),
            self._keywords,
            after=result.last,
            callback=self._on_search_result,
580
            user_data=(con, True))
581

582
    def _destroy(self, *args):
583 584
        if self._source_id is not None:
            GLib.source_remove(self._source_id)
585
        self._destroyed = True
586 587


588
class ContactRow(Gtk.ListBoxRow):
589 590
    def __init__(self, account, contact, jid, name, show_account,
                 groupchat=False):
591 592
        Gtk.ListBoxRow.__init__(self)
        self.get_style_context().add_class('start-chat-row')
593
        self.account = account
594
        self.account_label = app.get_account_label(account)
595 596 597 598 599 600 601
        self.show_account = show_account
        self.jid = jid
        self.contact = contact
        self.name = name
        self.groupchat = groupchat
        self.new = jid == ''

602
        show = contact.show if contact else 'offline'
603 604 605 606 607

        grid = Gtk.Grid()
        grid.set_column_spacing(12)
        grid.set_size_request(260, -1)

608
        image = self._get_avatar_image(account, jid, show)
609
        image.set_size_request(AvatarSize.CHAT, AvatarSize.CHAT)
610
        grid.add(image)
611 612 613 614 615 616

        middle_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        middle_box.set_hexpand(True)

        if self.name is None:
            if self.groupchat:
617
                self.name = _('Join Group Chat')
618
            else:
619
                self.name = _('Add Contact')
620

Philipp Hörist's avatar
Philipp Hörist committed
621
        self.name_label = Gtk.Label(label=self.name)
622 623
        self.name_label.set_ellipsize(Pango.EllipsizeMode.END)
        self.name_label.set_xalign(0)
624
        self.name_label.set_width_chars(20)
625 626 627 628
        self.name_label.set_halign(Gtk.Align.START)
        self.name_label.get_style_context().add_class('bold16')
        middle_box.add(self.name_label)

Philipp Hörist's avatar
Philipp Hörist committed
629
        self.jid_label = Gtk.Label(label=jid)
630
        self.jid_label.set_tooltip_text(jid)
631 632
        self.jid_label.set_ellipsize(Pango.EllipsizeMode.END)
        self.jid_label.set_xalign(0)
633
        self.jid_label.set_width_chars(22)
634
        self.jid_label.set_halign(Gtk.Align.START)
635
        self.jid_label.get_style_context().add_class('dim-label')
636 637
        middle_box.add(self.jid_label)

638
        grid.add(middle_box)
639 640

        if show_account:
641 642 643 644 645 646
            account_icon = Gtk.Image.new_from_icon_name(
                'org.gajim.Gajim-symbolic', Gtk.IconSize.MENU)
            account_icon.set_tooltip_text(
                _('Account: %s' % self.account_label))
            account_class = app.css_config.get_dynamic_class(account)
            account_icon.get_style_context().add_class(account_class)
647 648
            right_box = Gtk.Box()
            right_box.set_vexpand(True)
649
            right_box.add(account_icon)
650
            grid.add(right_box)
651

652
        self.add(grid)
653 654
        self.show_all()

655
    def _get_avatar_image(self, account, jid, show):
656 657 658 659 660
        if self.new:
            icon_name = 'avatar-default'
            if self.groupchat:
                icon_name = get_icon_name('muc-inactive')
            return Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DND)
661

662
        scale = self.get_scale_factor()
663 664 665 666 667 668
        if self.groupchat:
            surface = app.interface.avatar_storage.get_muc_surface(
                account, jid, AvatarSize.CHAT, scale)
            return Gtk.Image.new_from_surface(surface)

        avatar = app.contacts.get_avatar(
669
            account, jid, AvatarSize.CHAT, scale, show)
670 671
        return Gtk.Image.new_from_surface(avatar)

672 673 674 675 676 677 678 679 680 681
    def update_jid(self, jid):
        self.jid = jid
        self.jid_label.set_text(jid)

    def get_search_text(self):
        if self.contact is None and not self.groupchat:
            return self.jid
        if self.show_account:
            return '%s %s %s' % (self.name, self.jid, self.account_label)
        return '%s %s' % (self.name, self.jid)
682 683


684
class GlobalSearch(Gtk.ListBox):
685 686 687 688 689
    def __init__(self):
        Gtk.ListBox.__init__(self)
        self.set_has_tooltip(True)
        self.set_activate_on_single_click(False)
        self._progress = None
690
        self._add_placeholder()
691 692
        self.show_all()

693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709
    def _add_placeholder(self):
        placeholder = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
        placeholder.set_halign(Gtk.Align.CENTER)
        placeholder.set_valign(Gtk.Align.CENTER)
        icon = Gtk.Image.new_from_icon_name('system-search-symbolic',
                                            Gtk.IconSize.DIALOG)
        icon.get_style_context().add_class('dim-label')
        label = Gtk.Label(label=_('Search for group chats globally\n'
                                  '(press Return to start search)'))
        label.get_style_context().add_class('dim-label')
        label.set_justify(Gtk.Justification.CENTER)
        label.set_max_width_chars(35)
        placeholder.add(icon)
        placeholder.add(label)
        placeholder.show_all()
        self.set_placeholder(placeholder)

710 711 712 713 714 715
    def remove_all(self):
        def remove(row):
            self.remove(row)
            row.destroy()
        self.foreach(remove)

716
    def remove_progress(self):
717 718 719 720 721 722 723 724 725 726 727 728
        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)
729 730 731 732 733
        if self.get_selected_row() is None:
            row = self.get_row_at_index(1)
            if row is not None:
                self.select_row(row)
                row.grab_focus()
734 735
        self._progress.update()

736 737 738 739
    def _select(self, direction):
        selected_row = self.get_selected_row()
        if selected_row is None:
            return
740

741 742 743 744 745
        index = selected_row.get_index()
        if direction == 'next':
            index += 1
        else:
            index -= 1
746

747 748 749 750 751 752 753 754 755 756 757 758
        new_selected_row = self.get_row_at_index(index)
        if new_selected_row is None:
            return

        self.select_row(new_selected_row)
        new_selected_row.grab_focus()

    def select_next(self):
        self._select('next')

    def select_prev(self):
        self._select('prev')
759 760 761 762 763 764 765


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')
766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781
        self.new = False
        self.jid = item.jid
        self.groupchat = True

        name_label = Gtk.Label(label=item.name)
        name_label.set_halign(Gtk.Align.START)
        name_label.set_ellipsize(Pango.EllipsizeMode.END)
        name_label.set_max_width_chars(40)
        name_label.get_style_context().add_class('bold16')
        jid_label = Gtk.Label(label=item.jid)
        jid_label.set_halign(Gtk.Align.START)
        jid_label.set_ellipsize(Pango.EllipsizeMode.END)
        jid_label.set_max_width_chars(40)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        box.add(name_label)
        box.add(jid_label)
782 783 784 785 786 787 788 789 790 791 792

        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')
793
        self._text = _('%s group chats found')
794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819
        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()