view src/cagou/plugins/plugin_wid_chat.py @ 44:7819e9efa250

chat: avatar and nick are now displayed, need further aesthetic improvments
author Goffi <goffi@goffi.org>
date Mon, 29 Aug 2016 01:23:49 +0200
parents 286865bc013a
children b0595a33465d
line wrap: on
line source

#!/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.core.image import Image
from cagou import G


PLUGIN_INFO = {
    "name": _(u"chat"),
    "main": "Chat",
    "description": _(u"instant messaging with one person or a group"),
    "icon_small": u"{media}/icons/muchoslava/png/chat_rouge_32.png",
    "icon_medium": u"{media}/icons/muchoslava/png/chat_rouge_44.png"
}


class MessAvatar(Image):
    pass


class MessageWidget(BoxLayout):
    mess_data = properties.ObjectProperty()
    mess_label = properties.ObjectProperty()
    mess_box = properties.ObjectProperty()

    def __init__(self, **kwargs):
        BoxLayout.__init__(self, orientation='vertical', **kwargs)
        avatar = MessAvatar(source=self.mess_data.avatar)
        if self.mess_data.own_mess:
            self.mess_box.add_widget(avatar, len(self.mess_box.children))
        else:
            self.mess_box.add_widget(avatar)

    @property
    def chat(self):
        """return parent Chat instance"""
        return self.mess_data.parent

    @property
    def message(self):
        """Return currently displayed message"""
        return self.mess_data.main_message

    def sizeAdjust(self):
        """this widget grows up with its children"""
        text_width, text_height = self.mess_label.texture_size
        other_width = sum([c.width for c in self.mess_box.children if c != self.mess_label])
        if text_width + other_width > self.parent.width:
            self.mess_label.text_size = (self.parent.width - other_width - 10, None)
            self.text_max = text_width
        elif self.mess_label.text_size[0] is not None and text_width + other_width < self.parent.width - 10:
            if text_width > self.text_max:
                self.mess_label.text_size = (None, None)
            else:
                self.mess_label.text_size = (self.parent.width - other_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, do_scroll_x=False)
        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 _mucJoinCb(self, bare):
        jid_ = jid.JID(bare)
        self.changeWidget(jid_)

    def _mucJoinEb(self, failure):
        log.warning(u"Can't join room: {}".format(failure))

    def changeWidget(self, jid_):
        """change current widget for a new one with given jid

        @param jid_(jid.JID): jid of the widget to create
        """
        plugin_info = G.host.getPluginInfo(main=Chat)
        factory = plugin_info['factory']
        G.host.switchWidget(self, factory(plugin_info, jid_, profiles=[self.profile]))
        self.header_input.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]]:
                G.host.bridge.mucJoin(unicode(jid_), "", "", self.profile, callback=self._mucJoinCb, errback=self._mucJoinEb)
            else:
                self.changeWidget(jid_)

        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)

    def onDelete(self, force=False):
        if force==True:
            return True
        if len(list(G.host.widgets.getWidgets(self.__class__, self.target, profiles=self.profiles))) > 1:
            # we don't keep duplicate widgets
            return True
        return False


PLUGIN_INFO["factory"] = Chat.factory
quick_widgets.register(quick_chat.QuickChat, Chat)