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