1# This file is part of Gajim. 2# 3# Gajim is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published 5# by the Free Software Foundation; version 3 only. 6# 7# Gajim is distributed in the hope that it will be useful, 8# but WITHOUT ANY WARRANTY; without even the implied warranty of 9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10# GNU General Public License for more details. 11# 12# You should have received a copy of the GNU General Public License 13# along with Gajim. If not, see <http://www.gnu.org/licenses/>. 14 15import time 16 17from gi.repository import Gdk 18from gi.repository import GLib 19from gi.repository import Gtk 20 21from nbxmpp.namespaces import Namespace 22 23from gajim.common import app 24from gajim.common.i18n import _ 25from gajim.common.i18n import Q_ 26from gajim.common.helpers import open_uri 27from gajim.common.helpers import get_groupchat_name 28from gajim.common.const import RFC5646_LANGUAGE_TAGS 29from gajim.common.const import AvatarSize 30 31from .util import get_builder 32from .util import make_href_markup 33 34 35MUC_FEATURES = { 36 'muc_open': ( 37 'feather-globe-symbolic', 38 Q_('?Group chat feature:Open'), 39 _('Anyone can join this group chat')), 40 'muc_membersonly': ( 41 'feather-user-check-symbolic', 42 Q_('?Group chat feature:Members Only'), 43 _('This group chat is restricted ' 44 'to members only')), 45 'muc_nonanonymous': ( 46 'feather-shield-off-symbolic', 47 Q_('?Group chat feature:Not Anonymous'), 48 _('All other group chat participants ' 49 'can see your XMPP address')), 50 'muc_semianonymous': ( 51 'feather-shield-symbolic', 52 Q_('?Group chat feature:Semi-Anonymous'), 53 _('Only moderators can see your XMPP address')), 54 'muc_moderated': ( 55 'feather-mic-off-symbolic', 56 Q_('?Group chat feature:Moderated'), 57 _('Participants entering this group chat need ' 58 'to request permission to send messages')), 59 'muc_unmoderated': ( 60 'feather-mic-symbolic', 61 Q_('?Group chat feature:Not Moderated'), 62 _('Participants entering this group chat are ' 63 'allowed to send messages')), 64 'muc_public': ( 65 'feather-eye-symbolic', 66 Q_('?Group chat feature:Public'), 67 _('Group chat can be found via search')), 68 'muc_hidden': ( 69 'feather-eye-off-symbolic', 70 Q_('?Group chat feature:Hidden'), 71 _('This group chat can not be found via search')), 72 'muc_passwordprotected': ( 73 'feather-lock-symbolic', 74 Q_('?Group chat feature:Password Required'), 75 _('This group chat ' 76 'does require a password upon entry')), 77 'muc_unsecured': ( 78 'feather-unlock-symbolic', 79 Q_('?Group chat feature:No Password Required'), 80 _('This group chat does not require ' 81 'a password upon entry')), 82 'muc_persistent': ( 83 'feather-hard-drive-symbolic', 84 Q_('?Group chat feature:Persistent'), 85 _('This group chat persists ' 86 'even if there are no participants')), 87 'muc_temporary': ( 88 'feather-clock-symbolic', 89 Q_('?Group chat feature:Temporary'), 90 _('This group chat will be destroyed ' 91 'once the last participant left')), 92 'mam': ( 93 'feather-server-symbolic', 94 Q_('?Group chat feature:Archiving'), 95 _('Messages are archived on the server')), 96} 97 98 99class GroupChatInfoScrolled(Gtk.ScrolledWindow): 100 def __init__(self, account=None, options=None): 101 Gtk.ScrolledWindow.__init__(self) 102 if options is None: 103 options = {} 104 105 self._minimal = options.get('minimal', False) 106 107 self.set_size_request(options.get('width', 400), -1) 108 self.set_halign(Gtk.Align.CENTER) 109 110 if self._minimal: 111 self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) 112 else: 113 self.set_vexpand(True) 114 self.set_min_content_height(400) 115 self.set_policy(Gtk.PolicyType.NEVER, 116 Gtk.PolicyType.AUTOMATIC) 117 118 self._account = account 119 self._info = None 120 121 self._ui = get_builder('groupchat_info_scrolled.ui') 122 self.add(self._ui.info_grid) 123 self._ui.connect_signals(self) 124 self.show_all() 125 126 def get_account(self): 127 return self._account 128 129 def set_account(self, account): 130 self._account = account 131 132 def get_jid(self): 133 return self._info.jid 134 135 def set_author(self, author, epoch_timestamp=None): 136 has_author = bool(author) 137 if has_author and epoch_timestamp is not None: 138 time_ = time.strftime('%c', time.localtime(epoch_timestamp)) 139 author = f'{author} - {time_}' 140 141 self._ui.author.set_text(author or '') 142 self._ui.author.set_visible(has_author) 143 self._ui.author_label.set_visible(has_author) 144 145 def set_subject(self, subject): 146 has_subject = bool(subject) 147 subject = GLib.markup_escape_text(subject or '') 148 self._ui.subject.set_markup(make_href_markup(subject)) 149 self._ui.subject.set_visible(has_subject) 150 self._ui.subject_label.set_visible(has_subject) 151 152 def set_from_disco_info(self, info): 153 self._info = info 154 # Set name 155 if self._account is None: 156 name = info.muc_name 157 else: 158 con = app.connections[self._account] 159 name = get_groupchat_name(con, info.jid) 160 self._ui.name.set_text(name) 161 self._ui.name.set_visible(True) 162 163 # Set avatar 164 surface = app.interface.avatar_storage.get_muc_surface( 165 self._account, 166 str(info.jid), 167 AvatarSize.GROUP_INFO, 168 self.get_scale_factor()) 169 self._ui.avatar_image.set_from_surface(surface) 170 171 # Set description 172 has_desc = bool(info.muc_description) 173 self._ui.description.set_text(info.muc_description or '') 174 self._ui.description.set_visible(has_desc) 175 self._ui.description_label.set_visible(has_desc) 176 177 # Set address 178 self._ui.address.set_text(str(info.jid)) 179 180 if self._minimal: 181 return 182 183 # Set subject 184 self.set_subject(info.muc_subject) 185 186 # Set user 187 has_users = info.muc_users is not None 188 self._ui.users.set_text(info.muc_users or '') 189 self._ui.users.set_visible(has_users) 190 self._ui.users_image.set_visible(has_users) 191 192 # Set contacts 193 self._ui.contact_box.foreach(self._ui.contact_box.remove) 194 has_contacts = bool(info.muc_contacts) 195 if has_contacts: 196 for contact in info.muc_contacts: 197 self._ui.contact_box.add(self._get_contact_button(contact)) 198 199 self._ui.contact_box.set_visible(has_contacts) 200 self._ui.contact_label.set_visible(has_contacts) 201 202 # Set discussion logs 203 has_log_uri = bool(info.muc_log_uri) 204 self._ui.logs.set_uri(info.muc_log_uri or '') 205 self._ui.logs.set_label(_('Website')) 206 self._ui.logs.set_visible(has_log_uri) 207 self._ui.logs_label.set_visible(has_log_uri) 208 209 # Set room language 210 has_lang = bool(info.muc_lang) 211 lang = '' 212 if has_lang: 213 lang = RFC5646_LANGUAGE_TAGS.get(info.muc_lang, info.muc_lang) 214 self._ui.lang.set_text(lang) 215 self._ui.lang.set_visible(has_lang) 216 self._ui.lang_image.set_visible(has_lang) 217 218 self._add_features(info.features) 219 220 def _add_features(self, features): 221 grid = self._ui.info_grid 222 for row in range(30, 9, -1): 223 # Remove everything from row 30 to 10 224 # We probably will never have 30 rows and 225 # there is no method to count grid rows 226 grid.remove_row(row) 227 features = list(features) 228 229 if Namespace.MAM_2 in features: 230 features.append('mam') 231 232 row = 10 233 234 for feature in MUC_FEATURES: 235 if feature in features: 236 icon, name, tooltip = MUC_FEATURES.get(feature, 237 (None, None, None)) 238 if icon is None: 239 continue 240 grid.attach(self._get_feature_icon(icon, tooltip), 0, row, 1, 1) 241 grid.attach(self._get_feature_label(name), 1, row, 1, 1) 242 row += 1 243 grid.show_all() 244 245 def _on_copy_address(self, _button): 246 clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) 247 clipboard.set_text(f'xmpp:{self._info.jid}?join', -1) 248 249 @staticmethod 250 def _on_activate_log_link(button): 251 open_uri(button.get_uri()) 252 return Gdk.EVENT_STOP 253 254 def _on_activate_contact_link(self, button): 255 open_uri(f'xmpp:{button.get_uri()}?message', account=self._account) 256 return Gdk.EVENT_STOP 257 258 @staticmethod 259 def _on_activate_subject_link(_label, uri): 260 # We have to use this, because the default GTK handler 261 # is not cross-platform compatible 262 open_uri(uri) 263 return Gdk.EVENT_STOP 264 265 @staticmethod 266 def _get_feature_icon(icon, tooltip): 267 image = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.MENU) 268 image.set_valign(Gtk.Align.CENTER) 269 image.set_halign(Gtk.Align.END) 270 image.set_tooltip_text(tooltip) 271 return image 272 273 @staticmethod 274 def _get_feature_label(text): 275 label = Gtk.Label(label=text, use_markup=True) 276 label.set_halign(Gtk.Align.START) 277 label.set_valign(Gtk.Align.START) 278 return label 279 280 def _get_contact_button(self, contact): 281 button = Gtk.LinkButton.new(contact) 282 button.set_halign(Gtk.Align.START) 283 button.get_style_context().add_class('link-button') 284 button.connect('activate-link', self._on_activate_contact_link) 285 button.show() 286 return button 287