Mercurial > libervia-web
diff libervia/pages/_browser/invitation.py @ 1331:fe353fceec38
browser (invitation, photos/album): invitation manager improvments:
invitation manager has been moved to a separated module, it is generic so it can be used
with other activities.
It has been simplified, and contact to add are dynamically filtered using a text input.
Invitation are done by email using a the new modal.
New notification module is used to indicate when invitation has been done.
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 14 Aug 2020 09:31:32 +0200 |
parents | |
children | c74e5a488af6 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/_browser/invitation.py Fri Aug 14 09:31:32 2020 +0200 @@ -0,0 +1,317 @@ +from browser import document, window, timer +from bridge import Bridge +from template import Template +from dialog import notification +from cache import cache + +bridge = Bridge() + + +class InvitationManager: + + def __init__(self, invitation_type, invitation_data): + self.invitation_type = invitation_type + self.invitation_data = invitation_data + manager_panel_tpl = Template('invitation/manager.html') + self.manager_panel_elt = manager_panel_tpl.get_elt() + self.invite_by_email_tpl = Template('invitation/invite_by_email.html') + self.affiliation_tpl = Template('invitation/affiliation_item.html') + self.new_item_tpl = Template('invitation/new_item.html') + # list of item passing filter when adding a new contact + self._filtered_new_items = {} + self._active_new_item = None + self._idx = 0 + + def attach(self, affiliations=None): + if affiliations is None: + affiliations = {} + self.affiliations = affiliations + self.side_panel = self.manager_panel_elt.select_one( + '.invitation_manager_side_panel') + self.open() + for close_elt in self.manager_panel_elt.select('.click_to_close'): + close_elt.bind("click", self.on_manager_close) + self.side_panel.bind("click", lambda evt: evt.stopPropagation()) + + cache.fill_identities(affiliations.keys(), callback=self._set_affiliations) + + contact_elt = self.manager_panel_elt.select_one('input[name="contact"]') + contact_elt.bind("input", self.on_contact_input) + contact_elt.bind("keydown", self.on_contact_keydown) + contact_elt.bind("focus", self.on_contact_focus) + contact_elt.bind("blur", self.on_contact_blur) + document['invite_email'].bind('click', self.on_invite_email_click) + + def _set_affiliations(self): + for entity_jid, affiliation in self.affiliations.items(): + self.set_affiliation(entity_jid, affiliation) + + def open(self): + """Re-attach and show a closed panel""" + self._body_ori_style = document.body.style.height, document.body.style.overflow + document.body.style.height = '100vh' + document.body.style.overflow = 'hidden' + document.body <= self.manager_panel_elt + timer.set_timeout(lambda: self.side_panel.classList.add("open"), 0) + + def _on_close_transition_end(self, evt): + self.manager_panel_elt.remove() + # FIXME: not working with Brython, to report upstream + # self.side_panel.unbind("transitionend", self._on_close_transition_end) + self.side_panel.unbind("transitionend") + + def close(self): + """Hide the panel""" + document.body.style.height, document.body.style.overflow = self._body_ori_style + self.side_panel.classList.remove('open') + self.side_panel.bind("transitionend", self._on_close_transition_end) + + def invite_jid(self, entity_jid): + if self.invitation_type == 'photos': + path = self.invitation_data["path"] + service = self.invitation_data["service"] + album_name = path.rsplit('/')[-1] + print(f"inviting {entity_jid}") + bridge.FISInvite( + entity_jid, + service, + "photos", + "", + path, + album_name, + '', + callback=lambda entity_jid=entity_jid: + self._on_jid_invitation_success(entity_jid), + errback=lambda e: notification.show(f"invitation failed: {e}", "error") + ) + + def on_manager_close(self, evt): + self.close() + + def _on_jid_invitation_success(self, entity_jid): + form_elt = document['invitation_form'] + contact_elt = form_elt.select_one('input[name="contact"]') + contact_elt.value = "" + contact_elt.dispatchEvent(window.Event.new('input')) + notification.show( + f"{entity_jid} has been invited", + level="success", + ) + if entity_jid not in self.affiliations: + self.set_affiliation(entity_jid, "member") + + def on_contact_invite(self, evt, entity_jid): + """User is adding a contact""" + form_elt = document['invitation_form'] + contact_elt = form_elt.select_one('input[name="contact"]') + contact_elt.value = "" + contact_elt.dispatchEvent(window.Event.new('input')) + self.invite_jid(entity_jid) + + def on_contact_keydown(self, evt): + if evt.key == "Escape": + evt.target.value = "" + evt.target.dispatchEvent(window.Event.new('input')) + elif evt.key == "ArrowDown": + evt.stopPropagation() + evt.preventDefault() + content_elt = document['invitation_contact_search'].select_one( + ".search_dialog__content") + if self._active_new_item == None: + self._active_new_item = content_elt.firstElementChild + self._active_new_item.classList.add('selected') + else: + next_item = self._active_new_item.nextElementSibling + if next_item is not None: + self._active_new_item.classList.remove('selected') + self._active_new_item = next_item + self._active_new_item.classList.add('selected') + elif evt.key == "ArrowUp": + evt.stopPropagation() + evt.preventDefault() + content_elt = document['invitation_contact_search'].select_one( + ".search_dialog__content") + if self._active_new_item == None: + self._active_new_item = content_elt.lastElementChild + self._active_new_item.classList.add('selected') + else: + previous_item = self._active_new_item.previousElementSibling + if previous_item is not None: + self._active_new_item.classList.remove('selected') + self._active_new_item = previous_item + self._active_new_item.classList.add('selected') + elif evt.key == "Enter": + evt.stopPropagation() + evt.preventDefault() + if self._active_new_item is not None: + entity_jid = self._active_new_item.dataset.entityJid + self.invite_jid(entity_jid) + + def on_contact_focus(self, evt): + search_dialog = document['invitation_contact_search'] + search_dialog.classList.add('open') + self._active_new_item = None + evt.target.dispatchEvent(window.Event.new('input')) + + def on_contact_blur(self, evt): + search_dialog = document['invitation_contact_search'] + search_dialog.classList.remove('open') + for elt in self._filtered_new_items.values(): + elt.remove() + self._filtered_new_items.clear() + + + def on_contact_input(self, evt): + text = evt.target.value.strip().lower() + search_dialog = document['invitation_contact_search'] + content_elt = search_dialog.select_one(".search_dialog__content") + for (entity_jid, identity) in cache.identities.items(): + if not cache.match_identity(entity_jid, text, identity): + # if the entity was present in last pass, we remove it + try: + filtered_item = self._filtered_new_items.pop(entity_jid) + except KeyError: + pass + else: + filtered_item.remove() + continue + if entity_jid not in self._filtered_new_items: + # we only create a new element if the item was not already there + new_item_elt = self.new_item_tpl.get_elt({ + "entity_jid": entity_jid, + "identities": cache.identities, + }) + content_elt <= new_item_elt + self._filtered_new_items[entity_jid] = new_item_elt + for elt in new_item_elt.select('.click_to_ok'): + # we use mousedown instead of click because otherwise it would be + # ignored due to "blur" event manager (see + # https://stackoverflow.com/a/9335401) + elt.bind( + "mousedown", + lambda evt, entity_jid=entity_jid: self.on_contact_invite( + evt, entity_jid), + ) + + if ((self._active_new_item is not None + and not self._active_new_item.parentElement)): + # active item has been filtered out + self._active_new_item = None + + def _on_email_invitation_success(self, invitee_jid, email, name): + self.set_affiliation(invitee_jid, "member") + notification.show( + f"{name} has been invited, he/she has received an email with a link", + level="success", + ) + + def invitationSimpleCreateCb(self, invitation_data, email, name): + if self.invitation_type == 'photos': + path = self.invitation_data["path"] + service = self.invitation_data["service"] + invitee_jid = invitation_data['jid'] + album_name = path.rsplit('/')[-1] + bridge.FISInvite( + invitee_jid, + service, + "photos", + "", + path, + album_name, + '', + callback=lambda: self._on_email_invitation_success(invitee_jid, email, name), + errback=lambda e: window.alert(f"invitation failed for {email}: {e}") + ) + + # we update identities to have the name instead of the invitation jid in + # affiliations + cache.identities[invitee_jid] = {'nicknames': [name]} + cache.update() + + def invite_by_email(self, email, name): + guest_url_tpl = f'{window.URL.new("/g", document.baseURI).href}/{{uuid}}' + bridge.invitationSimpleCreate( + email, + name, + guest_url_tpl, + '', + callback=lambda data: self.invitationSimpleCreateCb(data, email, name), + errback=lambda e: window.alert(f"can't send email invitation: {e}") + ) + + def on_invite_email_submit(self, evt, invite_email_elt): + evt.stopPropagation() + evt.preventDefault() + form = document['email_invitation_form'] + try: + reportValidity = form.reportValidity + except AttributeError: + print("reportValidity is not supported by this browser!") + else: + if not reportValidity(): + return + email = form.select_one('input[name="email"]').value + name = form.select_one('input[name="name"]').value + self.invite_by_email(email, name) + invite_email_elt.remove() + self.open() + + def on_invite_email_close(self, evt, invite_email_elt): + evt.stopPropagation() + evt.preventDefault() + invite_email_elt.remove() + self.open() + + def on_invite_email_click(self, evt): + evt.stopPropagation() + evt.preventDefault() + invite_email_elt = self.invite_by_email_tpl.get_elt() + document.body <= invite_email_elt + document['email_invitation_submit'].bind( + 'click', lambda evt: self.on_invite_email_submit(evt, invite_email_elt) + ) + for close_elt in invite_email_elt.select('.click_to_close'): + close_elt.bind( + "click", lambda evt: self.on_invite_email_close(evt, invite_email_elt)) + self.close() + + ## affiliations + + def set_affiliation(self, entity_jid, affiliation): + if affiliation not in ('owner', 'member'): + raise NotImplementedError( + f'{affiliation} affiliation can not be set with this method for the ' + 'moment') + if entity_jid not in self.affiliations: + self.affiliations[entity_jid] = affiliation + affiliation_elt = self.affiliation_tpl.get_elt({ + "entity_jid": entity_jid, + "affiliation": affiliation, + "identities": cache.identities, + }) + document['affiliations'] <= affiliation_elt + for elt in affiliation_elt.select(".click_to_delete"): + elt.bind( + "click", + lambda evt, entity_jid=entity_jid, affiliation_elt=affiliation_elt: + self.on_affiliation_remove(entity_jid, affiliation_elt) + ) + + def _on_affiliation_remove_success(self, affiliation_elt, entity_jid): + affiliation_elt.remove() + del self.affiliations[entity_jid] + + def on_affiliation_remove(self, entity_jid, affiliation_elt): + if self.invitation_type == 'photos': + path = self.invitation_data["path"] + service = self.invitation_data["service"] + bridge.FISAffiliationsSet( + service, + "", + path, + {entity_jid: "none"}, + callback=lambda: self._on_affiliation_remove_success( + affiliation_elt, entity_jid), + errback=lambda e: notification.show( + f"can't remove affiliation: {e}", "error") + )