Philipp Hörist pushed to branch master at gajim / gajim
Commits: 0c9cd26a by Daniel Brötzmann at 2019-07-04T21:26:31Z Add Create Group Chat Dialog
- - - - -
7 changed files:
- gajim/app_actions.py - gajim/application.py - gajim/common/const.py - gajim/data/gui/application_menu.ui - + gajim/data/gui/groupchat_creation.ui - + gajim/gtk/groupchat_creation.py - gajim/roster_window.py
Changes:
===================================== gajim/app_actions.py ===================================== @@ -31,6 +31,7 @@ from gajim.gtk.server_info import ServerInfoDialog from gajim.gtk.mam_preferences import MamPreferences from gajim.gtk.preferences import Preferences from gajim.gtk.join_groupchat import JoinGroupchatWindow +from gajim.gtk.groupchat_creation import CreateGroupchatWindow from gajim.gtk.start_chat import StartChatDialog from gajim.gtk.add_contact import AddNewContactWindow from gajim.gtk.single_message import SingleMessageWindow @@ -151,6 +152,14 @@ def on_join_gc(_action, param): window.present()
+def on_create_gc(_action, param): + window = app.get_app_window(CreateGroupchatWindow) + if window is None: + CreateGroupchatWindow(param.get_string() or None) + else: + window.present() + + def on_add_contact(_action, param): account, jid = param.get_strv() if not jid:
===================================== gajim/application.py ===================================== @@ -424,6 +424,10 @@ class GajimApplication(Gtk.Application): act.connect("activate", app_actions.open_link) self.add_action(act)
+ act = Gio.SimpleAction.new('create-groupchat', GLib.VariantType.new('s')) + act.connect("activate", app_actions.on_create_gc) + self.add_action(act) + act = Gio.SimpleAction.new('browse-history', GLib.VariantType.new('a{sv}')) act.connect("activate", app_actions.on_browse_history)
===================================== gajim/common/const.py ===================================== @@ -2,6 +2,7 @@ from enum import IntEnum, Enum, unique from collections import namedtuple
from gajim.common.i18n import _ +from gajim.common.i18n import Q_
EncryptionData = namedtuple('EncryptionData', 'additional_data') EncryptionData.__new__.__defaults__ = (None,) # type: ignore @@ -209,6 +210,25 @@ class MUCJoinedState(Enum): return self.name
+MUC_CREATION_EXAMPLES = [ + (Q_('?Group chat name:Team'), + Q_('?Group chat description:Project discussion'), + Q_('?Group chat address:team')), + (Q_('?Group chat name:Family'), + Q_('?Group chat description:Spring gathering'), + Q_('?Group chat address:family')), + (Q_('?Group chat name:Vacation'), + Q_('?Group chat description:Trip planning'), + Q_('?Group chat address:vacation')), + (Q_('?Group chat name:Repairs'), + Q_('?Group chat description:Local help group'), + Q_('?Group chat address:repairs')), + (Q_('?Group chat name:News'), + Q_('?Group chat description:Local news and reports'), + Q_('?Group chat address:news')), +] + + EME_MESSAGES = { 'urn:xmpp:otr:0': _('This message was encrypted with OTR '
===================================== gajim/data/gui/application_menu.ui ===================================== @@ -77,6 +77,12 @@ <attribute name="action">app.start-chat</attribute> <attribute name="accel"><Primary>N</attribute> </item> + <item> + <attribute name="label" translatable="yes">Create Group Chat</attribute> + <attribute name="action">app.create-groupchat</attribute> + <attribute name="accel"><Primary>G</attribute> + <attribute name="target"></attribute> + </item> <item> <attribute name="label" translatable="yes">Bookmarks</attribute> <attribute name="action">app.bookmarks</attribute>
===================================== gajim/data/gui/groupchat_creation.ui ===================================== @@ -0,0 +1,344 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <object class="GtkListStore" id="account_liststore"> + <columns> + <!-- column-name id --> + <column type="gchararray"/> + <!-- column-name label --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkPopover" id="public_private_popover"> + <property name="can_focus">False</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="border_width">12</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Public or Private?</property> + <style> + <class name="bold16"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Private:</b> If you plan to chat encrypted, you would typically want to create a private group chat. You have to invite people so they can join.</property> + <property name="use_markup">True</property> + <property name="wrap">True</property> + <property name="width_chars">40</property> + <property name="max_width_chars">40</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Public:</b> Anyone who knows the address can join.</property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkBox" id="create_group_chat"> + <property name="width_request">400</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">18</property> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Create Group Chat</property> + <property name="wrap">True</property> + <style> + <class name="large-header"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="grid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">12</property> + <property name="row_spacing">6</property> + <property name="column_spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">end</property> + <property name="label" translatable="yes">_Name</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">name_entry</property> + <style> + <class name="dim-label"/> + </style> + </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="halign">end</property> + <property name="label" translatable="yes">_Description</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">description_entry</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="name_entry"> + <property name="width_request">200</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="description_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="show_emoji_icon">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">end</property> + <property name="valign">center</property> + <property name="margin_top">12</property> + <property name="label" translatable="yes">_Public</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">public_switch</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkSwitch" id="public_switch"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="valign">center</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkMenuButton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">end</property> + <property name="relief">none</property> + <property name="popover">public_private_popover</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">dialog-information-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">end</property> + <property name="label" translatable="yes">_Address</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">address_entry</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="address_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <signal name="activate" handler="_on_address_entry_activate" swapped="no"/> + <signal name="changed" handler="_on_address_entry_changed" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="account_combo"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="valign">center</property> + <property name="model">account_liststore</property> + <property name="id_column">0</property> + <signal name="changed" handler="_on_account_combo_changed" swapped="no"/> + <child> + <object class="GtkCellRendererText"/> + <attributes> + <attribute name="text">1</attribute> + </attributes> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="account_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="halign">end</property> + <property name="label" translatable="yes">_Account</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">account_combo</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">end</property> + <property name="margin_top">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkSpinner" id="spinner"> + <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="GtkButton" id="create_button"> + <property name="label" translatable="yes">_Create</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">end</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="_on_create_clicked" swapped="no"/> + <style> + <class name="suggested-action"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">3</property> + </packing> + </child> + </object> +</interface>
===================================== gajim/gtk/groupchat_creation.py ===================================== @@ -0,0 +1,245 @@ +# 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 logging +import random + +from gi.repository import Gtk +from gi.repository import Gdk + +from nbxmpp.protocol import InvalidJid +from nbxmpp.protocol import JID +from nbxmpp.util import is_error_result + +from gajim.common import app +from gajim.common.const import MUC_CREATION_EXAMPLES +from gajim.common.i18n import _ + +from gajim.gtk.dialogs import ErrorDialog +from gajim.gtk.util import get_builder +from gajim.gtk.util import ensure_not_destroyed + +log = logging.getLogger('gajim.gtk.groupchat_creation') + + +class CreateGroupchatWindow(Gtk.ApplicationWindow): + def __init__(self, account): + Gtk.ApplicationWindow.__init__(self) + self.set_name('CreateGroupchat') + self.set_application(app.app) + self.set_position(Gtk.WindowPosition.CENTER) + self.set_show_menubar(False) + self.set_resizable(True) + self.set_title(_('Create Group Chat')) + + self._ui = get_builder('groupchat_creation.ui') + self.add(self._ui.create_group_chat) + + self._destroyed = False + + not_muc_error = _('Address does not belong to a group chat server') + self._error_messages = { + 'remote-server-not-found': _('Remote server not found'), + 'remote-server-timeout': _('Remote server timeout'), + 'service-unavailable': not_muc_error, + 'subscription-required': not_muc_error, + 'not-muc-service': not_muc_error, + 'already-exists': _('Group chat already exists') + } + + self._account = self._fill_account_combo(account) + + self._create_entry_completion() + self._fill_placeholders() + + self._ui.connect_signals(self) + self.connect('key-press-event', self._on_key_press_event) + self.connect('destroy', self._on_destroy) + + self.show_all() + self.set_focus(self._ui.address_entry) + + def _fill_account_combo(self, account): + accounts = app.get_enabled_accounts_with_labels() + account_liststore = self._ui.account_combo.get_model() + for acc in accounts: + account_liststore.append(acc) + + # Hide account combobox if there is only one account + if len(accounts) == 1: + self._ui.account_combo.hide() + self._ui.account_label.hide() + + if account is None: + account = accounts[0][0] + + self._ui.account_combo.set_active_id(account) + return account + + def _create_entry_completion(self): + entry_completion = Gtk.EntryCompletion() + model = Gtk.ListStore(str) + entry_completion.set_model(model) + entry_completion.set_text_column(0) + + entry_completion.set_inline_completion(True) + entry_completion.set_popup_single_match(False) + + self._ui.address_entry.set_completion(entry_completion) + + def _fill_placeholders(self): + placeholder = random.choice(MUC_CREATION_EXAMPLES) + server = app.get_muc_domain(self._account) or 'muc.example.com' + + self._ui.name_entry.set_placeholder_text( + placeholder[0] + _(' (optional)...')) + self._ui.description_entry.set_placeholder_text( + placeholder[1] + _(' (optional)...')) + self._ui.address_entry.set_placeholder_text( + '%s@%s' % (placeholder[2], server)) + + def _on_key_press_event(self, _widget, event): + if event.keyval == Gdk.KEY_Escape: + self.destroy() + + def _on_account_combo_changed(self, combo): + self._account = combo.get_active_id() + self._fill_placeholders() + + def _update_entry_completion(self, entry, text): + text = entry.get_text() + if '@' in text: + text = text.split('@', 1)[0] + + model = entry.get_completion().get_model() + model.clear() + + server = app.get_muc_domain(self._account) or 'muc.example.com' + model.append(['%s@%s' % (text, server)]) + + def _validate_jid(self, text): + if not text: + self._set_warning_icon(False) + self._ui.create_button.set_sensitive(False) + return + + try: + jid = JID(text) + if jid.getResource(): + raise InvalidJid + + except InvalidJid: + self._set_warning(_('Invalid Address')) + else: + self._set_warning_icon(False) + self._ui.create_button.set_sensitive(True) + + def _set_processing_state(self, enabled): + if enabled: + self._ui.spinner.start() + self._ui.create_button.set_sensitive(False) + else: + self._ui.spinner.stop() + self._ui.grid.set_sensitive(not enabled) + + def _set_warning_icon(self, enabled): + icon = 'dialog-warning-symbolic' if enabled else None + self._ui.address_entry.set_icon_from_icon_name( + Gtk.EntryIconPosition.SECONDARY, icon) + + def _set_warning_tooltip(self, text): + self._ui.address_entry.set_icon_tooltip_text( + Gtk.EntryIconPosition.SECONDARY, text) + + def _set_warning(self, text): + self._set_warning_icon(True) + self._set_warning_tooltip(text) + self._ui.create_button.set_sensitive(False) + + def _set_warning_from_error(self, error): + text = self._error_messages.get(error.type, str(error)) + self._set_warning(text) + + def _set_warning_from_error_code(self, error_code): + self._set_warning(self._error_messages[error_code]) + + def _on_address_entry_changed(self, entry): + text = entry.get_text() + self._update_entry_completion(entry, text) + self._validate_jid(text) + + def _on_server_entry_changed(self, _widget): + self._update_create_button() + + def _on_address_entry_activate(self, _widget): + self._on_create_clicked() + + def _on_create_clicked(self, *args): + if not app.account_is_connected(self._account): + ErrorDialog( + _('Not Connected'), + _('You have to be connected to create a group chat.')) + return + + room_jid = self._ui.address_entry.get_text() + + self._set_processing_state(True) + con = app.connections[self._account] + con.get_module('Discovery').disco_info( + room_jid, callback=self._disco_info_received) + + @ensure_not_destroyed + def _disco_info_received(self, result): + if is_error_result(result): + if result.type == 'item-not-found': + self._create_muc(result.jid) + return + self._set_warning_from_error(result) + + else: + self._set_warning_from_error_code( + 'already-exists' if result.is_muc else 'not-muc-service') + + self._set_processing_state(False) + + def _create_muc(self, room_jid): + name = self._ui.name_entry.get_text() + description = self._ui.description_entry.get_text() + is_public = self._ui.public_switch.get_active() + + config = { + # XEP-0045 options + 'muc#roomconfig_roomname': name, + 'muc#roomconfig_roomdesc': description, + 'muc#roomconfig_publicroom': is_public, + 'muc#roomconfig_membersonly': not is_public, + 'muc#roomconfig_whois': 'moderators' if is_public else 'anyone', + 'muc#roomconfig_changesubject': not is_public, + + # Ejabberd options + 'public_list': is_public, + } + + # Create new group chat by joining + app.interface.join_gc_room( + self._account, + str(room_jid), + app.nicks[self._account], + None, + config=config) + + self.destroy() + + def _on_destroy(self, *args): + self._destroyed = True
===================================== gajim/roster_window.py ===================================== @@ -3558,6 +3558,13 @@ class RosterWindow: app.app.activate_action('%s-join-groupchat' % account, GLib.Variant('as', [account, '']))
+ def on_create_gc_activate(self, widget, account): + """ + When the create gc menuitem is clicked, show the create gc window + """ + app.app.activate_action('create-groupchat', + GLib.Variant('s', account)) + def on_show_transports_action(self, action, param): app.config.set('show_transports_group', param.get_boolean()) action.set_state(param) @@ -5342,7 +5349,9 @@ class RosterWindow: """ item = Gtk.MenuItem.new_with_mnemonic(_('_Join New Group Chat')) item.connect('activate', self.on_join_gc_activate, account) - + gc_sub_menu.append(item) + item = Gtk.MenuItem.new_with_mnemonic(_('_Create Group Chat')) + item.connect('activate', self.on_create_gc_activate, account) gc_sub_menu.append(item)
# User has at least one bookmark.
View it on GitLab: https://dev.gajim.org/gajim/gajim/commit/0c9cd26a0111976f17b32f59fda533cee08...