changeset 19:e8e3704eb97f

Added basic chat panel - the chat panel show history, timestamp, and nickname (pretty similar to primitivus and wix chat window) - JID has be rewritten to work with pyjamas, and is now in browser_side directory - a widget can now be selected: the message send in uniBox will be sent to it if there is no explicit target prefix ("@something") - a basic status panel is added under the uniBox, but not used yet
author Goffi <goffi@goffi.org>
date Sat, 16 Apr 2011 01:46:01 +0200
parents 795d144fc1d2
children 8f4b1a8914c3
files browser_side/contact.py browser_side/jid.py browser_side/panels.py libervia.py libervia.tac public/libervia.css tools/jid.py
diffstat 7 files changed, 244 insertions(+), 68 deletions(-) [+]
line wrap: on
line diff
--- a/browser_side/contact.py	Fri Apr 15 15:30:31 2011 +0200
+++ b/browser_side/contact.py	Sat Apr 16 01:46:01 2011 +0200
@@ -29,7 +29,7 @@
 
 from pyjamas.dnd import makeDraggable
 from pyjamas.ui.DragWidget import DragWidget, DragContainer
-from tools.jid import JID
+from jid import JID
 
 class DragLabel(DragWidget):
 
@@ -70,13 +70,14 @@
         DragLabel.__init__(self, group, "GROUP")
     
 
-class ContactLabel(Label):
+class ContactLabel(DragLabel, Label):
     def __init__(self, jid, name=None):
         if not name:
             name=jid
         Label.__init__(self, name)
         self.jid=jid
         self.setStyleName('contact')
+        DragLabel.__init__(self, jid, "CONTACT")
 
 class GroupList(VerticalPanel):
 
@@ -109,7 +110,7 @@
     def __init__(self, text):
         Label.__init__(self, text) #, Element=DOM.createElement('div')
         self.setStyleName('contactTitle')
-        DragLabel.__init__(self, text, "CONTACT")
+        DragLabel.__init__(self, text, "CONTACT_TITLE")
 
 class ContactPanel(SimplePanel):
     """Manage the contacts and groups"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/browser_side/jid.py	Sat Apr 16 01:46:01 2011 +0200
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+Libervia: a Salut à Toi frontend
+Copyright (C) 2011  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/>.
+"""
+
+
+
+class JID:
+    """This class help manage JID (Node@Domaine/Resource)"""
+
+    def __init__(self, jid):
+        self.__raw = str(jid)
+        self.__parse()
+
+    def __parse(self):
+        """find node domaine and resource"""
+        node_end=self.__raw.find('@')
+        if node_end<0:
+            node_end=0
+        domain_end=self.__raw.find('/')
+        if domain_end<1:
+            domain_end=len(self.__raw)
+        self.node=self.__raw[:node_end]
+        self.domain=self.__raw[(node_end+1) if node_end else 0:domain_end]
+        self.resource=self.__raw[domain_end+1:]
+        if not node_end:
+            self.bare=self.__raw
+        else:
+            self.bare=self.node+'@'+self.domain
+
+    def __str__(self):
+        return self.__raw.__str__()
+
+    def is_valid(self):
+        """return True if the jid is xmpp compliant"""
+        #FIXME: always return True for the moment
+        return True
--- a/browser_side/panels.py	Fri Apr 15 15:30:31 2011 +0200
+++ b/browser_side/panels.py	Sat Apr 16 01:46:01 2011 +0200
@@ -30,14 +30,16 @@
 from pyjamas.ui.MenuItem import MenuItem
 from pyjamas.ui.Label import Label
 from pyjamas.ui.DropWidget import DropWidget
+from pyjamas.ui.ClickListener import ClickHandler
 from pyjamas.ui import HasAlignment
 from pyjamas import Window
 from pyjamas import DOM
 
 from pyjamas.dnd import makeDraggable
 from pyjamas.ui.DragWidget import DragWidget, DragContainer
-from tools.jid import JID
+from jid import JID
 from datetime import datetime
+from time import time
 
 class MenuCmd:
 
@@ -115,21 +117,24 @@
             item_type = None
         DOM.eventPreventDefault(event)
         if item_type=="GROUP":
-            _mblog = MicroblogPanel(self.host, item)
-            _mblog.setAcceptedGroup(item)
+            _new_panel = MicroblogPanel(self.host, item)
+            _new_panel.setAcceptedGroup(item)
         elif item_type=="CONTACT":
-            _mblog = MicroblogPanel(self.host, accept_all=True)
+            _contact = JID(item)
+            _new_panel = ChatPanel(self.host, _contact)
+            _new_panel.historyPrint()
+        elif item_type=="CONTACT_TITLE":
+            _new_panel = MicroblogPanel(self.host, accept_all=True)
         self.host.mpanels.remove(self)
-        self.host.mpanels.append(_mblog)
+        self.host.mpanels.append(_new_panel)
         print "DEBUG"
         grid = self.getParent()
         row_idx, cell_idx = self._getCellAndRow(grid, event)
         self.removeFromParent()
-        grid.setWidget(row_idx, cell_idx, _mblog)
-        print "index:", row_idx, cell_idx
+        if self.host.selected == self:
+            self.host.select(None)
+        grid.setWidget(row_idx, cell_idx, _new_panel)
         #FIXME: delete object ? Check the right way with pyjamas
-        #self.host.middle_panel.changePanel(self.data,self.host.mpanels[0])
-        
 
 class EmptyPanel(DropCell, SimplePanel):
     """Empty dropable panel"""
@@ -157,7 +162,6 @@
         panel.setStyleName('microblogEntry')
         self.add(panel)
 
-
 class MicroblogPanel(DropCell, ScrollPanel):
 
     def __init__(self,host, title='&nbsp;', accept_all=False):
@@ -207,8 +211,89 @@
             if self.host.contactPanel.isContactInGroup(group, jid):
                 return True
         return False
+
+class StatusPanel(HTMLPanel):
+    def __init__(self, status=''):
+        self.status = status
+        HTMLPanel.__init__(self, self.__getContent())
+
+    def __getContent(self):
+        return "<span class='status'>%(status)s</span>" % {'status':self.status}
+
+    def changeStatus(self, new_status):
+        self.status = new_status
+        self.setHTML(self.__getContent())
+
+class ChatText(HTMLPanel):
+
+    def __init__(self, timestamp, nick, mymess, msg):
+        _date = datetime.fromtimestamp(float(timestamp or time()))
+        print "DEBUG"
+        print timestamp
+        print time()
+        print _date
+        _msg_class = ["chat_text_msg"]
+        if mymess:
+            _msg_class.append("chat_text_mymess")
+        HTMLPanel.__init__(self, "<span class='chat_text_timestamp'>%(timestamp)s</span> <span class='chat_text_nick'>%(nick)s</span> <span class='%(msg_class)s'>%(msg)s</span>" %
+            {"timestamp": _date.strftime("%H:%M"),
+            "nick": "[%s]" % nick,
+            "msg_class": ' '.join(_msg_class),
+            "msg": msg}
+            )
+        self.setStyleName('chatText')
     
+class ChatPanel(DropCell, ClickHandler, ScrollPanel):
 
+    def __init__(self, host, target, type='one2one'):
+        """Panel used for conversation (one 2 one or group chat)
+        @param host: SatWebFrontend instance
+        @param target: entity (JID) with who we have a conversation (contact's jid for one 2 one chat, or MUC room)
+        @param type: one2one for simple conversation, group for MUC"""
+        self.host = host
+        if not target:
+            print "ERROR: Empty target !"
+            return
+        self.target = target
+        title="%s" % target.bare
+        title.replace('<','&lt;').replace('>','&gt;')
+        _class = ['mb_panel_header']
+        ScrollPanel.__init__(self)
+        self.content = VerticalPanel()
+        self.content.add(HTMLPanel("<div class='%s'>%s</div>" % (','.join(_class),title)))
+        self.content.setWidth('100%')
+        self.setHeight('100%')
+        self.setStyleName('chatPanel')
+        self.add(self.content)
+        DropCell.__init__(self)
+        ClickHandler.__init__(self)
+        self.addClickListener(self)
+
+    def onClick(self, sender, event):
+        self.host.select(self)
+    
+    def setUserNick(self, nick):
+        """Set the nick of the user, usefull for e.g. change the color of the user"""
+        self.nick = nick
+    
+    def historyPrint(self, size=20):
+        """Print the initial history"""
+        def getHistoryCB(history):
+            stamps=history.keys()
+            stamps.sort()
+            for stamp in stamps: 
+                self.printMessage(history[stamp][0], history[stamp][1], stamp)
+        self.host.bridge.call('getHistory', getHistoryCB, self.host.whoami.bare, str(self.target), 20)
+    
+    def printMessage(self, from_jid, msg, timestamp=None):
+        """Print message in chat window. Must be implemented by child class"""
+        _jid=JID(from_jid)
+        nick = _jid.node 
+        mymess = _jid.bare == self.host.whoami.bare #mymess = True if message comes from local user
+        """if msg.startswith('/me '):
+            self.printInfo('* %s %s' % (nick, msg[4:]),type='me')
+            return"""
+        self.content.add(ChatText(timestamp, nick, mymess, msg))
 
 class MiddlePannel(HorizontalPanel):
     
@@ -241,11 +326,13 @@
 
         menu = Menu()
         uni_box = host.uniBox
+        status = host.statusPanel
         self.middle_panel = MiddlePannel(self.host)
         self.middle_panel.setWidth('100%')
 
         self.add(menu)
         self.add(uni_box)
+        self.add(status)
         self.add(self.middle_panel)
         
         self.setCellHeight(menu, "5%")
--- a/libervia.py	Fri Apr 15 15:30:31 2011 +0200
+++ b/libervia.py	Sat Apr 16 01:46:01 2011 +0200
@@ -28,7 +28,8 @@
 from pyjamas.ui.KeyboardListener import KEY_ENTER
 from browser_side.register import RegisterPanel, RegisterBox
 from browser_side.contact import ContactPanel
-from browser_side.panels import MainPanel, EmptyPanel, MicroblogPanel
+from browser_side.panels import MainPanel, EmptyPanel, MicroblogPanel, ChatPanel, StatusPanel
+from browser_side.jid import JID
 
 
 class LiberviaJsonProxy(JSONProxy):
@@ -66,7 +67,7 @@
 class BridgeCall(LiberviaJsonProxy):
     def __init__(self):
         LiberviaJsonProxy.__init__(self, "/json_api",
-                        ["getContacts", "sendMblog", "getMblogNodes"])
+                        ["getContacts", "sendMessage", "sendMblog", "getMblogNodes", "getProfileJid", "getHistory"])
 
 class BridgeSignals(LiberviaJsonProxy):
     def __init__(self):
@@ -84,8 +85,14 @@
         self.getCompletionItems().completions.append(key)
 
     def onKeyPress(self, sender, keycode, modifiers):
-        if keycode == KEY_ENTER and not self.visible: 
-            self.host.bridge.call('sendMblog', None, self.getText())
+        if keycode == KEY_ENTER and not self.visible:
+            _txt = self.getText()
+            if _txt:
+                if _txt.startswith('@'):
+                    self.host.bridge.call('sendMblog', None, self.getText())
+                elif isinstance(self.host.selected, ChatPanel):
+                    _chat = self.host.selected
+                    self.host.bridge.call('sendMessage', None, str(_chat.target), _txt, '', 'chat') 
             self.setText('')
 
     def complete(self):
@@ -96,10 +103,13 @@
 
 class SatWebFrontend:
     def onModuleLoad(self):
+        self.whoami = None
         self.bridge = BridgeCall()
         self.bridge_signals = BridgeSignals()
+        self.selected = None
         self.uniBox = UniBox(self)
         self.uniBox.addKey("@@: ")
+        self.statusPanel = StatusPanel()
         self.contactPanel = ContactPanel(self)
         self.panel = MainPanel(self)
         self.middle_panel = self.panel.middle_panel
@@ -112,6 +122,14 @@
         self._register = RegisterCall()
         self._register.call('isRegistered',self._isRegisteredCB)
 
+    def select(self, widget):
+        """Define the selected widget"""
+        if self.selected:
+            self.selected.removeStyleName('selected_widget')
+        self.selected = widget
+        if widget:
+            self.selected.addStyleName('selected_widget')
+
     def _isRegisteredCB(self, registered):
         if not registered:
             self._dialog = RegisterBox(self.logged,centered=True)
@@ -133,6 +151,8 @@
         #it's time to fill the page
         self.bridge.call('getContacts', self._getContactsCB)
         self.bridge_signals.call('getSignals', self._getSignalsCB)
+        #We want to know our own jid
+        self.bridge.call('getProfileJid', self._getProfileJidCB)
 
     def _getContactsCB(self, contacts_data):
         for contact in contacts_data:
@@ -146,6 +166,12 @@
         name,args = signal_data
         if name == 'personalEvent':
             self._personalEventCb(*args)
+        elif name == 'newMessage':
+            self._newMessageCb(*args)
+
+    def _getProfileJidCB(self, jid):
+        self.whoami = JID(jid)
+
 
     ## Signals callbacks ##
 
@@ -169,6 +195,13 @@
                     timestamp = float(data.get('timestamp',0)) #XXX: int doesn't work here
                     panel.addEntry(content, author, timestamp)
 
+    def _newMessageCb(self, from_jid, msg, msg_type, to_jid):
+        _from = JID(from_jid)
+        _to = JID(to_jid)
+        for panel in self.mpanels:
+            if isinstance(panel,ChatPanel) and (panel.target.bare == _from.bare or panel.target.bare == _to.bare):
+                panel.printMessage(_from, msg)
+
 if __name__ == '__main__':
     pyjd.setup("http://localhost:8080/libervia.html")
     app = SatWebFrontend()
--- a/libervia.tac	Fri Apr 15 15:30:31 2011 +0200
+++ b/libervia.tac	Sat Apr 16 01:46:01 2011 +0200
@@ -54,11 +54,22 @@
             fault = jsonrpclib.Fault(0, "Not allowed") #FIXME: define some standard error codes for libervia
             return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
         return jsonrpc.JSONRPC.render(self, request)
+
+    def jsonrpc_getProfileJid(self):
+        """Return the jid of the profile"""
+        profile = self.session.sat_profile
+        self.session.sat_jid = self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile)
+        return self.session.sat_jid
         
     def jsonrpc_getContacts(self):
         """Return all passed args."""
         profile = self.session.sat_profile
         return self.sat_host.bridge.getContacts(profile)
+    
+    def jsonrpc_sendMessage(self, to_jid, msg, subject, type):
+        """send message"""
+        profile = self.session.sat_profile
+        return self.sat_host.bridge.sendMessage(to_jid, msg, subject, type, profile)
 
     def jsonrpc_sendMblog(self, raw_text):
         """Parse raw_text of the microblog box, and send message consequently"""
@@ -74,6 +85,20 @@
             else:
                 return self.sat_host.bridge.sendGroupBlog([recip], text, profile)
 
+    def jsonrpc_getHistory(self, from_jid, to_jid, size):
+        """Return history for the from_jid/to_jid couple"""
+        #FIXME: this method should definitely be asynchrone, need to fix it !!!
+        profile = self.session.sat_profile
+        try:
+            _jid = JID(self.session.sat_jid)
+        except:
+            error("No jid saved for this profile")
+            return {}
+        if JID(from_jid).userhost() != _jid.userhost() and JID(to_jid) != _jid.userhost():
+            error("Trying to get history from a different jid, maybe a hack attempt ?")
+            return {}
+        return self.sat_host.bridge.getHistory(from_jid, to_jid, size) 
+
 
 
 class Register(jsonrpc.JSONRPC):
@@ -285,7 +310,7 @@
             sys.exit(1)
         self.bridge.register("connected", self.signal_handler.connected)
         self.bridge.register("connectionError", self.signal_handler.connectionError)
-        for signal_name in ['presenceUpdate', 'personalEvent']:
+        for signal_name in ['presenceUpdate', 'personalEvent', 'newMessage']:
             self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name))
         root.putChild('json_signal_api', self.signal_handler)
         root.putChild('json_api', MethodHandler(self))
--- a/public/libervia.css	Fri Apr 15 15:30:31 2011 +0200
+++ b/public/libervia.css	Sat Apr 16 01:46:01 2011 +0200
@@ -208,8 +208,35 @@
     font-style: italic;
 }
 
+/* Chat */
+
+.chatText {
+  /* font-size: smaller; */
+}
+
+.chat_text_timestamp {
+    font-style: italic;
+}
+
+.chat_text_nick {
+    font-weight: bold;
+}
+
+.chat_text_mymess {
+    color: blue;
+}
+
 /* Test drag and drop */
 
 .dragover {
     background: #8f8;
 }
+
+/* Misc */
+
+.selected_widget {
+    /* this property is set when a widget is the current target of the uniBox
+     * (messages entered in unibox will be sent to this widget)
+     */
+  border: 3px dashed red;
+}
--- a/tools/jid.py	Fri Apr 15 15:30:31 2011 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-"""
-Libervia: a Salut à Toi frontend
-Copyright (C) 2011  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/>.
-"""
-
-
-
-class JID(str):
-    """This class help manage JID (Node@Domaine/Resource)"""
-
-    def __init__(self, jid):
-        str.__init__(self, jid)
-        self.__parse()
-
-    def __parse(self):
-        """find node domaine and resource"""
-        node_end=self.find('@')
-        if node_end<0:
-            node_end=0
-        domain_end=self.find('/')
-        if domain_end<1:
-            domain_end=len(self)
-        self.node=self[:node_end]
-        self.domain=self[(node_end+1) if node_end else 0:domain_end]
-        self.resource=self[domain_end+1:]
-        if not node_end:
-            self.short=self
-        else:
-            self.short=self.node+'@'+self.domain
-
-    def is_valid(self):
-        """return True if the jid is xmpp compliant"""
-        #FIXME: always return True for the moment
-        return True