# HG changeset patch # User Goffi # Date 1302911161 -7200 # Node ID e8e3704eb97f3fc84f2fbfde01eb47117651001e # Parent 795d144fc1d2652c1892e31e1bcfaae42d5b9dc0 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 diff -r 795d144fc1d2 -r e8e3704eb97f browser_side/contact.py --- 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""" diff -r 795d144fc1d2 -r e8e3704eb97f browser_side/jid.py --- /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 . +""" + + + +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 diff -r 795d144fc1d2 -r e8e3704eb97f browser_side/panels.py --- 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=' ', 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 "%(status)s" % {'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, "%(timestamp)s %(nick)s %(msg)s" % + {"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('<','<').replace('>','>') + _class = ['mb_panel_header'] + ScrollPanel.__init__(self) + self.content = VerticalPanel() + self.content.add(HTMLPanel("
%s
" % (','.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%") diff -r 795d144fc1d2 -r e8e3704eb97f libervia.py --- 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() diff -r 795d144fc1d2 -r e8e3704eb97f libervia.tac --- 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)) diff -r 795d144fc1d2 -r e8e3704eb97f public/libervia.css --- 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; +} diff -r 795d144fc1d2 -r e8e3704eb97f tools/jid.py --- 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 . -""" - - - -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