changeset 22:74117b733bac

plugin chat: first draft: - only one 2 one display is handled for now - own messages are displayed in blue, other ones in gray - by default we display our own jid (for now) - to change target, contact in contact widget can be clicked, or a jid can be entered in header input - disco is called on jid entered in header, if it's a conference a MUCJoin will be called (not implemented yet), else a new chat widget with the jid replace the current one
author Goffi <goffi@goffi.org>
date Mon, 08 Aug 2016 01:07:43 +0200
parents 57bf68eacdb9
children f9869f34f629
files src/cagou/plugins/plugin_wid_chat.kv src/cagou/plugins/plugin_wid_chat.py src/cagou/plugins/plugin_wid_contact_list.py
diffstat 3 files changed, 245 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cagou/plugins/plugin_wid_chat.kv	Mon Aug 08 01:07:43 2016 +0200
@@ -0,0 +1,67 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program 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 Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+<MessagesWidget>:
+    spacing: self._spacing
+    padding: self._padding
+
+<MessageInputWidget>:
+    size_hint: 1,None
+    height: 40
+    hint_text: "Enter your message here"
+
+<MessageWidget>:
+    canvas.before:
+        Color:
+            rgba: 1, 1, 1, 1
+        BorderImage:
+            source: "cagou/images/border_{}.jpg".format("blue" if root.mess_data.own_mess else "gray")
+            pos: self.pos
+            size: self.size
+
+    mess_label: mess_label
+    size_hint: None,None
+    pos_hint: {'x': 0} if root.mess_data.own_mess else {'right': 1}
+    height: max(mess_label.height, 20)
+    width: mess_label.width
+    on_height: if root.parent: root.parent.sizeAdjust()
+    BoxLayout:
+        # Label:
+        #     id: nick_label
+        #     text: root.mess_data.nick
+        #     # text: unicode(self.texture_size)
+        #     padding: 5, 5
+        #     bold: True
+        #     # text_size: None, self.height
+        #     # height: 20
+        #     size_hint: None, None
+        #     size: self.texture_size
+        #     pos_hint: {'top': 0}
+        #     # width: self.texture_size[0]
+        #     # height: max(self.texture_size[1], mess_label.height)
+        #     # size_hint: None, 1
+        #     # valign: "top"
+        Label:
+            id: mess_label
+            color: 0, 0, 0, 1
+            padding: 5, 5
+            text_size: None, None
+            size_hint: None, None
+            size: self.texture_size
+            # text: 'root:{} nick:{} self:{}'.format(root.height, nick_label.height, self.height)
+            text: root.message or u'  '
+            # haligh: "left"
+            on_texture_size: root.adjustMax(self.texture_size)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cagou/plugins/plugin_wid_chat.py	Mon Aug 08 01:07:43 2016 +0200
@@ -0,0 +1,162 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program 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 Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from sat.core import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+from cagou.core.constants import Const as C
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.scrollview import ScrollView
+from kivy.uix.textinput import TextInput
+from kivy import properties
+from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.quick_frontend import quick_chat
+from sat_frontends.tools import jid
+from cagou.core import cagou_widget
+from cagou import G
+
+
+PLUGIN_INFO = {
+    "name": _(u"chat"),
+    "main": "Chat",
+    "description": _(u"instant messaging with one person or a group"),
+}
+
+
+class MessageWidget(BoxLayout):
+    mess_data = properties.ObjectProperty()
+    mess_label = properties.ObjectProperty(None)
+
+    def __init__(self, **kwargs):
+        BoxLayout.__init__(self, orientation='vertical', **kwargs)
+
+    @property
+    def message(self):
+        """Return currently displayed message"""
+        return self.mess_data.main_message
+
+    def adjustMax(self, texture_size):
+        """this widget grows up with its children"""
+        width, height = texture_size
+        if width > self.parent.width:
+            self.mess_label.text_size = (self.parent.width - 10, None)
+
+
+class MessageInputWidget(TextInput):
+
+    def _key_down(self, key, repeat=False):
+        displayed_str, internal_str, internal_action, scale = key
+        if internal_action == 'enter':
+            self.dispatch('on_text_validate')
+        else:
+            super(MessageInputWidget, self)._key_down(key, repeat)
+
+
+class MessagesWidget(BoxLayout):
+    _spacing = properties.NumericProperty(10)
+    _padding = properties.NumericProperty(5)
+
+    def __init__(self, **kwargs):
+        kwargs['orientation'] = 'vertical'
+        kwargs['size_hint'] = (1, None)
+        super(MessagesWidget, self).__init__(**kwargs)
+
+    def sizeAdjust(self):
+        self.height = sum([(c.height+self._padding*2) for c in self.children]) + self._spacing
+
+
+class Chat(quick_chat.QuickChat, cagou_widget.CagouWidget):
+
+    def __init__(self, host, target, type_=C.CHAT_ONE2ONE, occupants=None, subject=None, profiles=None):
+        quick_chat.QuickChat.__init__(self, host, target, type_, occupants, subject, profiles=profiles)
+        cagou_widget.CagouWidget.__init__(self)
+        self.header_input.hint_text = u"You are talking with {}".format(target)
+        scroll_view = ScrollView(size_hint=(1,0.8), scroll_y=0)
+        self.messages_widget = MessagesWidget()
+        scroll_view.add_widget(self.messages_widget)
+        self.add_widget(scroll_view)
+        message_input = MessageInputWidget()
+        message_input.bind(on_text_validate=self.onSend)
+        self.add_widget(message_input)
+        self.postInit()
+
+    @classmethod
+    def factory(cls, plugin_info, target, profiles):
+        profiles = list(profiles)
+        if len(profiles) > 1:
+            raise NotImplementedError(u"Multi-profiles is not available yet for chat")
+        if target is None:
+            target = G.host.profiles[profiles[0]].whoami
+        return G.host.widgets.getOrCreateWidget(cls, target, on_new_widget=None, on_existing_widget=C.WIDGET_RECREATE, profiles=profiles)
+
+    def messageDataConverter(self, idx, mess_id):
+        return {"mess_data": self.messages[mess_id]}
+
+    def _onHistoryPrinted(self):
+        """Refresh or scroll down the focus after the history is printed"""
+        # self.adapter.data = self.messages
+        for mess_data in self.messages.itervalues():
+            self.appendMessage(mess_data)
+        super(Chat, self)._onHistoryPrinted()
+
+    def createMessage(self, message):
+        self.appendMessage(message)
+
+    def appendMessage(self, mess_data):
+        self.messages_widget.add_widget(MessageWidget(mess_data=mess_data))
+
+    def onSend(self, input_widget):
+        G.host.messageSend(
+            self.target,
+            {'': input_widget.text}, # TODO: handle language
+            mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat
+            profile_key=self.profile
+            )
+        input_widget.text = ''
+
+    def onHeaderInput(self):
+        text = self.header_input.text.strip()
+        try:
+            if text.count(u'@') != 1 or text.count(u' '):
+                raise ValueError
+            jid_ = jid.JID(text)
+        except ValueError:
+            log.info(u"entered text is not a jid")
+            return
+
+        def discoCb(disco):
+            # TODO: check if plugin XEP-0045 is activated
+            if "conference" in [i[0] for i in disco[1]]:
+                raise NotImplementedError(u"MUC not implemented yet")
+                # G.host.bridge.MUCJoin(unicode(jid_), "", "", self.profile)
+            else:
+                plugin_info = [p for p in G.host.getPluggedWidgets() if p["factory"] == self.factory][0]  # FIXME: Q&D way, need a proper method in host
+                factory = plugin_info['factory']
+                G.host.switchWidget(self, factory(plugin_info, jid_, profiles=[self.profile]))
+
+        def discoEb(failure):
+            log.warning(u"Disco failure, ignore this text: {}".format(failure))
+
+        G.host.bridge.discoInfos(jid_.domain, self.profile, callback=discoCb, errback=discoEb)
+
+
+
+PLUGIN_INFO["factory"] = Chat.factory
+quick_widgets.register(quick_chat.QuickChat, Chat)
--- a/src/cagou/plugins/plugin_wid_contact_list.py	Mon Aug 08 01:02:32 2016 +0200
+++ b/src/cagou/plugins/plugin_wid_contact_list.py	Mon Aug 08 01:07:43 2016 +0200
@@ -22,6 +22,7 @@
 log = logging.getLogger(__name__)
 from sat.core.i18n import _
 from sat_frontends.quick_frontend.quick_contact_list import QuickContactList
+from sat_frontends.tools import jid
 from kivy.uix.boxlayout import BoxLayout
 from kivy.uix.listview import ListView
 from kivy.adapters.listadapter import ListAdapter
@@ -49,6 +50,21 @@
     def __init__(self, **kwargs):
         BoxLayout.__init__(self, **kwargs)
 
+    def on_touch_down(self, touch):
+        if self.collide_point(*touch.pos):
+            # XXX: for now clicking on an item launch the corresponding Chat widget
+            #      behaviour should change in the future
+            try:
+                # FIXME: Q&D way to get chat plugin, should be replaced by a clean method
+                #        in host
+                plg_infos = [p for p in G.host.getPluggedWidgets() if 'chat' in p['import_name']][0]
+            except IndexError:
+                log.warning(u"No plugin widget found to display chat")
+            else:
+                factory = plg_infos['factory']
+                G.host.switchWidget(self, factory(plg_infos, jid.JID(self.jid), profiles=iter(G.host.profiles)))
+
+
 
 class ContactList(QuickContactList, cagou_widget.CagouWidget):