profile.py 12 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1
import logging
Yann Leboulanger's avatar
Yann Leboulanger committed
2

Daniel Brötzmann's avatar
Daniel Brötzmann committed
3
from gi.repository import Gio
4
from gi.repository import Gdk
Daniel Brötzmann's avatar
Daniel Brötzmann committed
5
from gi.repository import Gtk
6
7
from gi.repository import GLib

Daniel Brötzmann's avatar
Daniel Brötzmann committed
8
9
10
11
from nbxmpp.errors import StanzaError
from nbxmpp.namespaces import Namespace
from nbxmpp.modules.vcard4 import VCard
from nbxmpp.modules.user_avatar import Avatar
12

13
14
from gajim.common import app
from gajim.common.const import AvatarSize
Daniel Brötzmann's avatar
Daniel Brötzmann committed
15
16
17
18
19
from gajim.common.i18n import _
from gajim.common.i18n import Q_

from gajim.gui.avatar import clip_circle
from gajim.gui.avatar_selector import AvatarSelector
20
from gajim.gui.dialogs import ErrorDialog
Daniel Brötzmann's avatar
Daniel Brötzmann committed
21
22
23
24
25
26
27
28
from gajim.gui.filechoosers import AvatarChooserDialog
from gajim.gui.util import get_builder
from gajim.gui.vcard_grid import VCardGrid
from gajim.gui.util import scroll_to_end

log = logging.getLogger('gajim.gui.profile')

MENU_DICT = {
29
    'fn': Q_('?profile:Full Name'),
Daniel Brötzmann's avatar
Daniel Brötzmann committed
30
    'bday': _('Birthday'),
31
32
    'gender': Q_('?profile:Gender'),
    'adr': Q_('?profile:Address'),
Daniel Brötzmann's avatar
Daniel Brötzmann committed
33
    'email': _('Email'),
34
    'impp': Q_('?profile:IM Address'),
Daniel Brötzmann's avatar
Daniel Brötzmann committed
35
36
37
38
39
    'tel': _('Phone No.'),
    'org': Q_('?profile:Organisation'),
    'title': Q_('?profile:Title'),
    'role': Q_('?profile:Role'),
    'url': _('URL'),
40
    'key': Q_('?profile:Public Encryption Key'),
Daniel Brötzmann's avatar
Daniel Brötzmann committed
41
}
Yann Leboulanger's avatar
Yann Leboulanger committed
42

43

44
class ProfileWindow(Gtk.ApplicationWindow):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
45
    def __init__(self, account, *args):
46
47
48
49
        Gtk.ApplicationWindow.__init__(self)
        self.set_application(app.app)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_show_menubar(False)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
50
51
52
53
        self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
        self.set_resizable(True)
        self.set_default_size(700, 600)
        self.set_name('ProfileWindow')
54
55
        self.set_title(_('Profile'))

56
        self.account = account
Daniel Brötzmann's avatar
Daniel Brötzmann committed
57
        self._jid = app.get_jid_from_account(account)
58

Daniel Brötzmann's avatar
Daniel Brötzmann committed
59
        self._ui = get_builder('profile.ui')
60

Daniel Brötzmann's avatar
Daniel Brötzmann committed
61
62
63
        menu = Gio.Menu()
        for action, label in MENU_DICT.items():
            menu.append(label, 'win.add-' + action.lower())
64

Daniel Brötzmann's avatar
Daniel Brötzmann committed
65
66
        self._ui.add_entry_button.set_menu_model(menu)
        self._add_actions()
67

Daniel Brötzmann's avatar
Daniel Brötzmann committed
68
69
        self._avatar_selector = None
        self._current_avatar = None
70
        self._current_vcard = None
Daniel Brötzmann's avatar
Daniel Brötzmann committed
71
        self._avatar_nick_public = None
72

Daniel Brötzmann's avatar
Daniel Brötzmann committed
73
74
75
76
        # False  - no change to avatar
        # None   - we want to delete the avatar
        # Avatar - upload new avatar
        self._new_avatar = False
77

Daniel Brötzmann's avatar
Daniel Brötzmann committed
78
        self._ui.nickname_entry.set_text(app.nicks[account])
79

80
81
        self._vcard_grid = VCardGrid(self.account)
        self._ui.profile_box.add(self._vcard_grid)
82

Daniel Brötzmann's avatar
Daniel Brötzmann committed
83
84
        self.add(self._ui.profile_stack)
        self.show_all()
85

Daniel Brötzmann's avatar
Daniel Brötzmann committed
86
        self._load_avatar()
87

Daniel Brötzmann's avatar
Daniel Brötzmann committed
88
89
90
        client = app.get_client(account)
        client.get_module('VCard4').request_vcard(
            callback=self._on_vcard_received)
91

Daniel Brötzmann's avatar
Daniel Brötzmann committed
92
93
94
95
        client.get_module('PubSub').get_access_model(
            Namespace.VCARD4_PUBSUB,
            callback=self._on_access_model_received,
            user_data=Namespace.VCARD4_PUBSUB)
96

Daniel Brötzmann's avatar
Daniel Brötzmann committed
97
98
99
100
        client.get_module('PubSub').get_access_model(
            Namespace.AVATAR_METADATA,
            callback=self._on_access_model_received,
            user_data=Namespace.AVATAR_METADATA)
101

Daniel Brötzmann's avatar
Daniel Brötzmann committed
102
103
104
105
        client.get_module('PubSub').get_access_model(
            Namespace.AVATAR_DATA,
            callback=self._on_access_model_received,
            user_data=Namespace.AVATAR_DATA)
106

Daniel Brötzmann's avatar
Daniel Brötzmann committed
107
108
109
110
        client.get_module('PubSub').get_access_model(
            Namespace.NICK,
            callback=self._on_access_model_received,
            user_data=Namespace.NICK)
111

Daniel Brötzmann's avatar
Daniel Brötzmann committed
112
113
        self._ui.connect_signals(self)
        self.connect('key-press-event', self._on_key_press_event)
114

Daniel Brötzmann's avatar
Daniel Brötzmann committed
115
116
    def _on_access_model_received(self, task):
        namespace = task.get_user_data()
117

Daniel Brötzmann's avatar
Daniel Brötzmann committed
118
119
120
121
122
        try:
            result = task.finish()
        except StanzaError as error:
            log.warning('Unable to get access model for %s: %s',
                        namespace, error)
123
124
            return

Daniel Brötzmann's avatar
Daniel Brötzmann committed
125
126
127
128
129
130
131
132
133
134
135
        access_model = result == 'open'

        if namespace == Namespace.VCARD4_PUBSUB:
            self._ui.vcard_access.set_active(access_model)
        else:
            if self._avatar_nick_public is None:
                self._avatar_nick_public = access_model
            else:
                self._avatar_nick_public = (self._avatar_nick_public and
                                            access_model)
            self._ui.avatar_nick_access.set_active(self._avatar_nick_public)
136

Daniel Brötzmann's avatar
Daniel Brötzmann committed
137
    def _on_vcard_received(self, task):
138
        try:
139
            self._current_vcard = task.finish()
Daniel Brötzmann's avatar
Daniel Brötzmann committed
140
141
        except StanzaError as error:
            log.info('Error loading VCard: %s', error)
142
            self._current_vcard = VCard()
143

144
145
146
        if self._current_vcard is None:
            self._current_vcard = VCard()

Daniel Brötzmann's avatar
Daniel Brötzmann committed
147
        self._load_avatar()
148
        self._vcard_grid.set_vcard(self._current_vcard.copy())
Daniel Brötzmann's avatar
Daniel Brötzmann committed
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
        self._ui.profile_stack.set_visible_child_name('profile')
        self._ui.spinner.stop()

    def _load_avatar(self):
        scale = self.get_scale_factor()
        self._current_avatar = app.contacts.get_avatar(
            self.account,
            self._jid,
            AvatarSize.VCARD,
            scale)

        self._ui.avatar_image.set_from_surface(self._current_avatar)
        self._ui.avatar_image.show()

    def _on_key_press_event(self, _widget, event):
        if event.keyval == Gdk.KEY_Escape:
165
166
            self.destroy()

Daniel Brötzmann's avatar
Daniel Brötzmann committed
167
168
169
170
171
172
173
174
175
176
    def _add_actions(self):
        for action in MENU_DICT:
            action_name = 'add-' + action.lower()
            act = Gio.SimpleAction.new(action_name, None)
            act.connect('activate', self._on_action)
            self.add_action(act)

    def _on_action(self, action, _param):
        name = action.get_name()
        key = name.split('-')[1]
177
        self._vcard_grid.add_new_property(key)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
178
179
180
        GLib.idle_add(scroll_to_end, self._ui.scrolled)

    def _on_edit_clicked(self, *args):
181
        self._vcard_grid.set_editable(True)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
182
183
184
185
186
187
188
189
190
191
192
        self._ui.edit_button.hide()
        self._ui.add_entry_button.set_no_show_all(False)
        self._ui.add_entry_button.show_all()
        self._ui.cancel_button.show()
        self._ui.save_button.show()
        self._ui.remove_avatar_button.show()
        self._ui.edit_avatar_button.show()
        self._ui.nickname_entry.set_sensitive(True)
        self._ui.privacy_button.show()

    def _on_cancel_clicked(self, _widget):
193
        self._vcard_grid.set_editable(False)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
194
195
196
197
198
199
200
201
202
203
        self._ui.edit_button.show()
        self._ui.add_entry_button.hide()
        self._ui.cancel_button.hide()
        self._ui.save_button.hide()
        self._ui.remove_avatar_button.hide()
        self._ui.edit_avatar_button.hide()
        self._ui.privacy_button.hide()
        self._ui.nickname_entry.set_sensitive(False)
        self._ui.avatar_image.set_from_surface(self._current_avatar)
        self._ui.nickname_entry.set_text(app.nicks[self.account])
204
        self._vcard_grid.set_vcard(self._current_vcard.copy())
Daniel Brötzmann's avatar
Daniel Brötzmann committed
205
206
207
208
209
210
211
212
213
214
215
216
217
218
        self._new_avatar = False

    def _on_save_clicked(self, _widget):
        self._ui.spinner.start()
        self._ui.profile_stack.set_visible_child_name('spinner')
        self._ui.add_entry_button.hide()
        self._ui.cancel_button.hide()
        self._ui.save_button.hide()
        self._ui.edit_button.show()
        self._ui.remove_avatar_button.hide()
        self._ui.edit_avatar_button.hide()
        self._ui.privacy_button.hide()
        self._ui.nickname_entry.set_sensitive(False)

219
        self._vcard_grid.validate()
Philipp Hörist's avatar
Philipp Hörist committed
220
        self._vcard_grid.sort()
221
222
223

        vcard = self._vcard_grid.get_vcard()
        self._current_vcard = vcard.copy()
Daniel Brötzmann's avatar
Daniel Brötzmann committed
224
225
226

        con = app.connections[self.account]
        con.get_module('VCard4').set_vcard(
227
            self._current_vcard,
Daniel Brötzmann's avatar
Daniel Brötzmann committed
228
229
230
231
232
233
234
235
            public=self._ui.vcard_access.get_active(),
            callback=self._on_save_finished)

        public = self._ui.avatar_nick_access.get_active()

        if self._new_avatar is False:
            if self._avatar_nick_public != public:
                con.get_module('UserAvatar').set_access_model(public)
236

Daniel Brötzmann's avatar
Daniel Brötzmann committed
237
238
        else:
            # Only update avatar if it changed
239
240
241
242
            con.get_module('UserAvatar').set_avatar(
                self._new_avatar,
                public=public,
                callback=self._on_set_avatar)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
243
244
245

        nick = GLib.markup_escape_text(self._ui.nickname_entry.get_text())
        con.get_module('UserNickname').set_nickname(nick, public=public)
246
247

        if not nick:
Daniel Brötzmann's avatar
Daniel Brötzmann committed
248
249
            nick = app.settings.get_account_setting(
                self.account, 'name')
250
        app.nicks[self.account] = nick
251

252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
    def _on_set_avatar(self, task):
        try:
            task.finish()
        except StanzaError as error:
            if self._new_avatar is None:
                # Trying to remove the avatar but the node does not exist
                if error.condition == 'item-not-found':
                    return

            title = _('Error while uploading avatar')
            text = error.get_text()

            if (error.condition == 'not-acceptable' and
                    error.app_condition == 'payload-too-big'):
                text = _('Avatar file size too big')

            ErrorDialog(title, text)

            self._ui.avatar_image.set_from_surface(self._current_avatar)
            self._new_avatar = False
            return

Daniel Brötzmann's avatar
Daniel Brötzmann committed
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
    def _on_remove_avatar(self, _button):
        contact = app.contacts.create_contact(self._jid, self.account)
        scale = self.get_scale_factor()
        surface = app.interface.avatar_storage.get_surface(
            contact, AvatarSize.VCARD, scale, default=True)

        self._ui.avatar_image.set_from_surface(surface)
        self._ui.remove_avatar_button.hide()
        self._new_avatar = None

    def _on_edit_avatar(self, button):
        def _on_file_selected(path):
            if self._avatar_selector is None:
                self._avatar_selector = AvatarSelector()
                self._ui.avatar_selector_box.add(self._avatar_selector)

            self._avatar_selector.prepare_crop_area(path)
            self._ui.avatar_update_button.set_sensitive(
                self._avatar_selector.get_prepared())
            self._ui.profile_stack.set_visible_child_name('avatar_selector')

        AvatarChooserDialog(_on_file_selected,
                            transient_for=button.get_toplevel())

    def _on_cancel_update_avatar(self, _button):
        self._ui.profile_stack.set_visible_child_name('profile')

    def _on_update_avatar(self, _button):
        success, data, width, height = self._avatar_selector.get_avatar_bytes()
        if not success:
304
305
306
            self._ui.profile_stack.set_visible_child_name('profile')
            ErrorDialog(_('Error while processing image'),
                        _('Failed to generate avatar.'))
Daniel Brötzmann's avatar
Daniel Brötzmann committed
307
308
309
310
            return

        sha = app.interface.avatar_storage.save_avatar(data)
        if sha is None:
311
312
313
            self._ui.profile_stack.set_visible_child_name('profile')
            ErrorDialog(_('Error while processing image'),
                        _('Failed to generate avatar.'))
Daniel Brötzmann's avatar
Daniel Brötzmann committed
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
            return

        self._new_avatar = Avatar()
        self._new_avatar.add_image_source(data, 'image/png', height, width)

        scale = self.get_scale_factor()
        surface = app.interface.avatar_storage.surface_from_filename(
            sha, AvatarSize.VCARD, scale)

        self._ui.avatar_image.set_from_surface(clip_circle(surface))
        self._ui.remove_avatar_button.show()
        self._ui.profile_stack.set_visible_child_name('profile')

    def _access_switch_toggled(self, *args):
        avatar_nick_access = self._ui.avatar_nick_access.get_active()
        vcard_access = self._ui.vcard_access.get_active()
        self._ui.avatar_nick_access_label.set_text(
            _('Everyone') if avatar_nick_access else _('Contacts'))
        self._ui.vcard_access_label.set_text(
            _('Everyone') if vcard_access else _('Contacts'))

    def _on_save_finished(self, task):
        try:
            task.finish()
        except StanzaError as err:
            log.error('Could not publish VCard: %s', err)
            # TODO Handle error
            return

343
        self._vcard_grid.set_editable(False)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
344
345
        self._ui.profile_stack.set_visible_child_name('profile')
        self._ui.spinner.stop()