# Copyright 2007-2008 One Laptop Per Child # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA from gettext import gettext as _ import hippo import gtk import pango import logging import re import cjson import time from datetime import datetime from activity import ViewSourceActivity from sugar.activity.activity import Activity, SCOPE_PRIVATE from sugar.activity import widgets from sugar.graphics.alert import NotifyAlert from sugar.graphics.style import (Color, COLOR_BLACK, COLOR_WHITE, COLOR_BUTTON_GREY, FONT_BOLD, FONT_NORMAL) from sugar.graphics.roundbox import CanvasRoundBox from sugar.graphics.xocolor import XoColor from sugar.graphics.palette import Palette, CanvasInvoker from sugar.graphics.menuitem import MenuItem from sugar.graphics.toolbarbox import ToolbarBox from sugar.util import timestamp_to_elapsed_string from telepathy.client import Connection, Channel from telepathy.interfaces import ( CHANNEL_INTERFACE, CHANNEL_INTERFACE_GROUP, CHANNEL_TYPE_TEXT, CONN_INTERFACE_ALIASING) from telepathy.constants import ( CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES, CHANNEL_TEXT_MESSAGE_TYPE_NORMAL) logger = logging.getLogger('chat-activity') URL_REGEXP = re.compile('((http|ftp)s?://)?' '(([-a-zA-Z0-9]+[.])+[-a-zA-Z0-9]{2,}|([0-9]{1,3}[.]){3}[0-9]{1,3})' '(:[1-9][0-9]{0,4})?(/[-a-zA-Z0-9/%~@&_+=;:,.?#]*[a-zA-Z0-9/])?') class Chat(ViewSourceActivity): def __init__(self, handle): super(Chat, self).__init__(handle) root = self.make_root() self.set_canvas(root) root.show_all() self.entry.grab_focus() # toolbar toolbar_box = ToolbarBox() self.set_toolbar_box(toolbar_box) toolbar_box.toolbar.insert(widgets.ActivityButton(self), -1) separator = gtk.SeparatorToolItem() separator.props.draw = False toolbar_box.toolbar.insert(separator, -1) toolbar_box.toolbar.insert(widgets.TitleEntry(self), -1) separator = gtk.SeparatorToolItem() separator.props.draw = False separator.set_expand(True) toolbar_box.toolbar.insert(separator, -1) share_button = widgets.ShareButton(self) toolbar_box.toolbar.insert(share_button, -1) toolbar_box.toolbar.insert(widgets.KeepButton(self), -1) toolbar_box.toolbar.insert(widgets.StopButton(self), -1) toolbar_box.show_all() # canvas self.owner = self._pservice.get_owner() self._chat_log = '' # Auto vs manual scrolling: self._scroll_auto = True self._scroll_value = 0.0 # Track last message, to combine several messages: self._last_msg = None self._last_msg_sender = None # Chat is room or one to one: self._chat_is_room = False self.text_channel = None if self.shared_activity: # we are joining the activity self.connect('joined', self._joined_cb) if self.get_shared(): # we have already joined self._joined_cb() elif handle.uri: # XMPP non-Sugar incoming chat, not sharable share_button.props.visible = False self._one_to_one_connection(handle.uri) else: # we are creating the activity if not self.metadata or self.metadata.get('share-scope', SCOPE_PRIVATE) == SCOPE_PRIVATE: # if we are in private session self._alert(_('Off-line'), _('Share, or invite someone.')) self.connect('shared', self._shared_cb) def _shared_cb(self, activity): logger.debug('Chat was shared') self._setup() def _one_to_one_connection(self, tp_channel): """Handle a private invite from a non-Sugar XMPP client.""" if self.shared_activity or self.text_channel: return bus_name, connection, channel = cjson.decode(tp_channel) logger.debug('GOT XMPP: %s %s %s', bus_name, connection, channel) conn = Connection( bus_name, connection, ready_handler=lambda conn: \ self._one_to_one_connection_ready_cb(bus_name, channel, conn)) def _one_to_one_connection_ready_cb(self, bus_name, channel, conn): """Callback for Connection for one to one connection""" text_channel = Channel(bus_name, channel) self.text_channel = TextChannelWrapper(text_channel, conn) self.text_channel.set_received_callback(self._received_cb) self.text_channel.handle_pending_messages() self.text_channel.set_closed_callback( self._one_to_one_connection_closed_cb) self._chat_is_room = False self._alert(_('On-line'), _('Private Chat')) # XXX How do we detect the sender going offline? self.entry.set_sensitive(True) self.entry.grab_focus() def _one_to_one_connection_closed_cb(self): """Callback for when the text channel closes.""" self._alert(_('Off-line'), _('left the chat')) def _setup(self): self.text_channel = TextChannelWrapper( self.shared_activity.telepathy_text_chan, self.shared_activity.telepathy_conn) self.text_channel.set_received_callback(self._received_cb) self._alert(_('On-line'), _('Connected')) self.shared_activity.connect('buddy-joined', self._buddy_joined_cb) self.shared_activity.connect('buddy-left', self._buddy_left_cb) self._chat_is_room = True self.entry.set_sensitive(True) self.entry.grab_focus() def _joined_cb(self, activity): """Joined a shared activity.""" if not self.shared_activity: return logger.debug('Joined a shared chat') for buddy in self.shared_activity.get_joined_buddies(): self._buddy_already_exists(buddy) self._setup() def _received_cb(self, buddy, text): """Show message that was received.""" if buddy: if type(buddy) is dict: nick = buddy['nick'] else: nick = buddy.props.nick else: nick = '???' logger.debug('Received message from %s: %s', nick, text) self.add_text(buddy, text) def _alert(self, title, text=None): alert = NotifyAlert(timeout=5) alert.props.title = title alert.props.msg = text self.add_alert(alert) alert.connect('response', self._alert_cancel_cb) alert.show() def _alert_cancel_cb(self, alert, response_id): self.remove_alert(alert) def _buddy_joined_cb (self, activity, buddy): """Show a buddy who joined""" if buddy == self.owner: return if buddy: nick = buddy.props.nick else: nick = '???' self.add_text(buddy, buddy.props.nick+' '+_('joined the chat'), status_message=True) def _buddy_left_cb (self, activity, buddy): """Show a buddy who joined""" if buddy == self.owner: return if buddy: nick = buddy.props.nick else: nick = '???' self.add_text(buddy, buddy.props.nick+' '+_('left the chat'), status_message=True) def _buddy_already_exists(self, buddy): """Show a buddy already in the chat.""" if buddy == self.owner: return if buddy: nick = buddy.props.nick else: nick = '???' self.add_text(buddy, buddy.props.nick+' '+_('is here'), status_message=True) def can_close(self): """Perform cleanup before closing. Close text channel of a one to one XMPP chat. """ if self._chat_is_room is False: if self.text_channel is not None: self.text_channel.close() return True def make_root(self): conversation = hippo.CanvasBox( spacing=0, background_color=COLOR_WHITE.get_int()) self.conversation = conversation entry = gtk.Entry() entry.modify_bg(gtk.STATE_INSENSITIVE, COLOR_WHITE.get_gdk_color()) entry.modify_base(gtk.STATE_INSENSITIVE, COLOR_WHITE.get_gdk_color()) entry.set_sensitive(False) entry.connect('activate', self.entry_activate_cb) self.entry = entry hbox = gtk.HBox() hbox.add(entry) sw = hippo.CanvasScrollbars() sw.set_policy(hippo.ORIENTATION_HORIZONTAL, hippo.SCROLLBAR_NEVER) sw.set_root(conversation) self.scrolled_window = sw vadj = self.scrolled_window.props.widget.get_vadjustment() vadj.connect('changed', self.rescroll) vadj.connect('value-changed', self.scroll_value_changed_cb) canvas = hippo.Canvas() canvas.set_root(sw) box = gtk.VBox(homogeneous=False) box.pack_start(canvas) box.pack_start(hbox, expand=False) return box def rescroll(self, adj, scroll=None): """Scroll the chat window to the bottom""" if self._scroll_auto: adj.set_value(adj.upper-adj.page_size) self._scroll_value = adj.get_value() def scroll_value_changed_cb(self, adj, scroll=None): """Turn auto scrolling on or off. If the user scrolled up, turn it off. If the user scrolled to the bottom, turn it back on. """ if adj.get_value() < self._scroll_value: self._scroll_auto = False elif adj.get_value() == adj.upper-adj.page_size: self._scroll_auto = True def _link_activated_cb(self, link): url = url_check_protocol(link.props.text) self._show_via_journal(url) def add_text(self, buddy, text, status_message=False): """Display text on screen, with name and colors. buddy -- buddy object or dict {nick: string, color: string} (The dict is for loading the chat log from the journal, when we don't have the buddy object any more.) text -- string, what the buddy said status_message -- boolean False: show what buddy said True: show what buddy did hippo layout: .------------- rb ---------------. | +name_vbox+ +----msg_vbox----+ | | | | | | | | | nick: | | +--msg_hbox--+ | | | | | | | text | | | | +---------+ | +------------+ | | | | | | | | +--msg_hbox--+ | | | | | text | url | | | | | +------------+ | | | +----------------+ | `--------------------------------' """ if buddy: if type(buddy) is dict: # dict required for loading chat log from journal nick = buddy['nick'] color = buddy['color'] else: nick = buddy.props.nick color = buddy.props.color try: color_stroke_html, color_fill_html = color.split(',') except ValueError: color_stroke_html, color_fill_html = ('#000000', '#888888') # Select text color based on fill color: color_fill_rgba = Color(color_fill_html).get_rgba() color_fill_gray = (color_fill_rgba[0] + color_fill_rgba[1] + color_fill_rgba[2])/3 color_stroke = Color(color_stroke_html).get_int() color_fill = Color(color_fill_html).get_int() if color_fill_gray < 0.5: text_color = COLOR_WHITE.get_int() else: text_color = COLOR_BLACK.get_int() else: nick = '???' # XXX: should be '' but leave for debugging color_stroke = COLOR_BLACK.get_int() color_fill = COLOR_WHITE.get_int() text_color = COLOR_BLACK.get_int() color = '#000000,#FFFFFF' self._add_log(nick, color, text, status_message) # Check for Right-To-Left languages: if pango.find_base_dir(nick, -1) == pango.DIRECTION_RTL: lang_rtl = True else: lang_rtl = False # Check if new message box or add text to previous: new_msg = True if self._last_msg_sender: if not status_message: if buddy == self._last_msg_sender: # Add text to previous message new_msg = False if not new_msg: rb = self._last_msg msg_vbox = rb.get_children()[1] msg_hbox = hippo.CanvasBox( orientation=hippo.ORIENTATION_HORIZONTAL) msg_vbox.append(msg_hbox) else: rb = CanvasRoundBox(background_color=color_fill, border_color=color_stroke, padding=4) rb.props.border_color = color_stroke # Bug #3742 self._last_msg = rb self._last_msg_sender = buddy if not status_message: name = hippo.CanvasText(text=nick+': ', color=text_color, font_desc=FONT_BOLD.get_pango_desc()) name_vbox = hippo.CanvasBox( orientation=hippo.ORIENTATION_VERTICAL) name_vbox.append(name) rb.append(name_vbox) msg_vbox = hippo.CanvasBox( orientation=hippo.ORIENTATION_VERTICAL) rb.append(msg_vbox) msg_hbox = hippo.CanvasBox( orientation=hippo.ORIENTATION_HORIZONTAL) msg_vbox.append(msg_hbox) if status_message: self._last_msg_sender = None match = URL_REGEXP.search(text) while match: # there is a URL in the text starttext = text[:match.start()] if starttext: message = hippo.CanvasText( text=starttext, size_mode=hippo.CANVAS_SIZE_WRAP_WORD, color=text_color, font_desc=FONT_NORMAL.get_pango_desc(), xalign=hippo.ALIGNMENT_START) msg_hbox.append(message) url = text[match.start():match.end()] message = hippo.CanvasLink( text=url, color=text_color, font_desc=FONT_BOLD.get_pango_desc(), ) attrs = pango.AttrList() attrs.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE, 0, 32767)) message.set_property("attributes", attrs) message.connect('activated', self._link_activated_cb) palette = URLMenu(url) palette.props.invoker = CanvasInvoker(message) msg_hbox.append(message) text = text[match.end():] match = URL_REGEXP.search(text) if text: message = hippo.CanvasText( text=text, size_mode=hippo.CANVAS_SIZE_WRAP_WORD, color=text_color, font_desc=FONT_NORMAL.get_pango_desc(), xalign=hippo.ALIGNMENT_START) msg_hbox.append(message) # Order of boxes for RTL languages: if lang_rtl: msg_hbox.reverse() if new_msg: rb.reverse() if new_msg: box = hippo.CanvasBox(padding=2) box.append(rb) self.conversation.append(box) def add_separator(self, timestamp): """Add whitespace and timestamp between chat sessions.""" box = hippo.CanvasBox(padding=2) time_with_current_year = (time.localtime(time.time())[0],) +\ time.strptime(timestamp, "%b %d %H:%M:%S")[1:] timestamp_seconds = time.mktime(time_with_current_year) if timestamp_seconds > time.time(): time_with_previous_year = (time.localtime(time.time())[0]-1,) +\ time.strptime(timestamp, "%b %d %H:%M:%S")[1:] timestamp_seconds = time.mktime(time_with_previous_year) message = hippo.CanvasText( text=timestamp_to_elapsed_string(timestamp_seconds), color=COLOR_BUTTON_GREY.get_int(), font_desc=FONT_NORMAL.get_pango_desc(), xalign=hippo.ALIGNMENT_CENTER) box.append(message) self.conversation.append(box) self._last_msg_sender = None self._add_log_timestamp(timestamp) def entry_activate_cb(self, entry): text = entry.props.text logger.debug('Entry: %s' % text) if text: self.add_text(self.owner, text) entry.props.text = '' if self.text_channel: self.text_channel.send(text) else: logger.debug('Tried to send message but text channel ' 'not connected.') def _add_log(self, nick, color, text, status_message): """Add the text to the chat log. nick -- string, buddy nickname color -- string, buddy.props.color text -- string, body of message status_message -- boolean """ if not nick: nick = '???' if not color: color = '#000000,#FFFFFF' if not text: text = '-' if not status_message: status_message = False self._chat_log += '%s\t%s\t%s\t%d\t%s\n' % ( datetime.strftime(datetime.now(), '%b %d %H:%M:%S'), nick, color, status_message, text) def _add_log_timestamp(self, existing_timestamp=None): """Add a timestamp entry to the chat log.""" if existing_timestamp is not None: self._chat_log += '%s\t\t\n' % existing_timestamp else: self._chat_log += '%s\t\t\n' % ( datetime.strftime(datetime.now(), '%b %d %H:%M:%S')) def _get_log(self): return self._chat_log def write_file(self, file_path): """Store chat log in Journal. Handling the Journal is provided by Activity - we only need to define this method. """ logger.debug('write_file: writing %s' % file_path) self._add_log_timestamp() f = open(file_path, 'w') try: f.write(self._get_log()) finally: f.close() self.metadata['mime_type'] = 'text/plain' def read_file(self, file_path): """Load a chat log from the Journal. Handling the Journal is provided by Activity - we only need to define this method. """ logger.debug('read_file: reading %s' % file_path) log = open(file_path).readlines() last_line_was_timestamp = False for line in log: if line.endswith('\t\t\n'): if last_line_was_timestamp is False: timestamp = line.strip().split('\t')[0] self.add_separator(timestamp) last_line_was_timestamp = True else: timestamp, nick, color, status, text = line.strip().split('\t') status_message = bool(int(status)) self.add_text({'nick': nick, 'color': color}, text, status_message) last_line_was_timestamp = False def _show_via_journal(self, url): """Ask the journal to display a URL""" import os import time from sugar import profile from sugar.activity.activity import show_object_in_journal from sugar.datastore import datastore logger.debug('Create journal entry for URL: %s', url) jobject = datastore.create() metadata = { 'title': "%s: %s" % (_('URL from Chat'), url), 'title_set_by_user': '1', 'icon-color': profile.get_color().to_string(), 'mime_type': 'text/uri-list', } for k,v in metadata.items(): jobject.metadata[k] = v file_path = os.path.join(self.get_activity_root(), 'instance', '%i_' % time.time()) open(file_path, 'w').write(url + '\r\n') os.chmod(file_path, 0755) jobject.set_file_path(file_path) datastore.write(jobject) show_object_in_journal(jobject.object_id) jobject.destroy() os.unlink(file_path) class TextChannelWrapper(object): """Wrap a telepathy Text Channel to make usage simpler.""" def __init__(self, text_chan, conn): """Connect to the text channel""" self._activity_cb = None self._activity_close_cb = None self._text_chan = text_chan self._conn = conn self._logger = logging.getLogger( 'chat-activity.TextChannelWrapper') self._signal_matches = [] m = self._text_chan[CHANNEL_INTERFACE].connect_to_signal( 'Closed', self._closed_cb) self._signal_matches.append(m) def send(self, text): """Send text over the Telepathy text channel.""" # XXX Implement CHANNEL_TEXT_MESSAGE_TYPE_ACTION if self._text_chan is not None: self._text_chan[CHANNEL_TYPE_TEXT].Send( CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, text) def close(self): """Close the text channel.""" self._logger.debug('Closing text channel') try: self._text_chan[CHANNEL_INTERFACE].Close() except: self._logger.debug('Channel disappeared!') self._closed_cb() def _closed_cb(self): """Clean up text channel.""" self._logger.debug('Text channel closed.') for match in self._signal_matches: match.remove() self._signal_matches = [] self._text_chan = None if self._activity_close_cb is not None: self._activity_close_cb() def set_received_callback(self, callback): """Connect the function callback to the signal. callback -- callback function taking buddy and text args """ if self._text_chan is None: return self._activity_cb = callback m = self._text_chan[CHANNEL_TYPE_TEXT].connect_to_signal('Received', self._received_cb) self._signal_matches.append(m) def handle_pending_messages(self): """Get pending messages and show them as received.""" for id, timestamp, sender, type, flags, text in \ self._text_chan[ CHANNEL_TYPE_TEXT].ListPendingMessages(False): self._received_cb(id, timestamp, sender, type, flags, text) def _received_cb(self, id, timestamp, sender, type, flags, text): """Handle received text from the text channel. Converts sender to a Buddy. Calls self._activity_cb which is a callback to the activity. """ if self._activity_cb: try: self._text_chan[CHANNEL_INTERFACE_GROUP] except: # One to one XMPP chat nick = self._conn[ CONN_INTERFACE_ALIASING].RequestAliases([sender])[0] buddy = {'nick': nick, 'color': '#000000,#808080'} else: # Normal sugar MUC chat # XXX: cache these buddy = self._get_buddy(sender) self._activity_cb(buddy, text) self._text_chan[ CHANNEL_TYPE_TEXT].AcknowledgePendingMessages([id]) else: self._logger.debug('Throwing received message on the floor' ' since there is no callback connected. See ' 'set_received_callback') def set_closed_callback(self, callback): """Connect a callback for when the text channel is closed. callback -- callback function taking no args """ self._activity_close_cb = callback def _get_buddy(self, cs_handle): """Get a Buddy from a (possibly channel-specific) handle.""" # XXX This will be made redundant once Presence Service # provides buddy resolution from sugar.presence import presenceservice # Get the Presence Service pservice = presenceservice.get_instance() # Get the Telepathy Connection tp_name, tp_path = pservice.get_preferred_connection() conn = Connection(tp_name, tp_path) group = self._text_chan[CHANNEL_INTERFACE_GROUP] my_csh = group.GetSelfHandle() if my_csh == cs_handle: handle = conn.GetSelfHandle() elif group.GetGroupFlags() & \ CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: handle = group.GetHandleOwners([cs_handle])[0] else: handle = cs_handle # XXX: deal with failure to get the handle owner assert handle != 0 return pservice.get_buddy_by_telepathy_handle( tp_name, tp_path, handle) class URLMenu(Palette): def __init__(self, url): Palette.__init__(self, url) self.url = url_check_protocol(url) menu_item = MenuItem(_('Copy to Clipboard'), 'edit-copy') menu_item.connect('activate', self._copy_to_clipboard_cb) self.menu.append(menu_item) menu_item.show() def _copy_to_clipboard_cb(self, menuitem): logger.debug('Copy %s to clipboard', self.url) clipboard = gtk.clipboard_get() targets = [("text/uri-list", 0, 0), ("UTF8_STRING", 0, 1)] if not clipboard.set_with_data(targets, self._clipboard_data_get_cb, self._clipboard_clear_cb, (self.url)): logger.error('GtkClipboard.set_with_data failed!') else: self.owns_clipboard = True def _clipboard_data_get_cb(self, clipboard, selection, info, data): logger.debug('_clipboard_data_get_cb data=%s target=%s', data, selection.target) if selection.target in ['text/uri-list']: if not selection.set_uris([data]): logger.debug('failed to set_uris') else: logger.debug('not uri') if not selection.set_text(data): logger.debug('failed to set_text') def _clipboard_clear_cb(self, clipboard, data): logger.debug('clipboard_clear_cb') self.owns_clipboard = False def url_check_protocol(url): """Check that the url has a protocol, otherwise prepend https:// url -- string Returns url -- string """ protocols = ['http://', 'https://', 'ftp://', 'ftps://'] no_protocol = True for protocol in protocols: if url.startswith(protocol): no_protocol = False if no_protocol: url = 'http://' + url return url ############# ACTIVITY META-INFORMATION ############### # this is used by Pippy to generate the Chat bundle. CHAT_ICON=\ """ ]> """ CHAT_NEWS=""" 60 * Version bump to allow for stable releases after v48 * #8772: Fix journal entry creation in Chat for uri-list (kevix) * #8471: Allow resuming Chat log in Write (morgs) * #8411: Add license to activity.info (morgs) * Remove parameter from bundlebuilder.start (morgs) * Add update_url for software updater (morgs) 45 * Updated translations: sl, nb, el, mr, rw, ur, ne * Fixed MANIFEST to include all translations (morgs) 44 * #7633: Close the text channel when stopping a 1-1 chat (morgs) * #7717: Log incoming messages (morgs) * #7692: Don't show pending messages when joining a chat (morgs) * Updated translations: nl, te, es, mn 43 * Updated translations: zh_TW, ja 42 * #6036: Show timestamp as elapsed time instead of date (morgs) * Updated translations: fr, mvo, pis, af, sd, pap, tpi, ar, de 41 * Updated translations: mr, de, ht, km, es, it * #6036: Add separator after old chat history (morgs) * #6298: Implement 1-1 private chat with non Sugar Jabber clients (morgs) 40 #5767: Use black text on light fill colors (matthias) 39 * ACK received messages (cassidy) * Handle pending messages when setting the message handler (cassidy) 38 * Updated translations: zh_TW, de, it 37 * UI Change: Merge multiple sequential messages from same author (morgs) * Updated translation: ar (pootle) * #6561: Fix RTL message alignment (Arabic) (khaled) 36 * #5053: Reduce white space around boxes (morgs) * #6621: set entry sensitive not editable (morgs) * Add license to activity.py (morgs) * #6743: border around gtk.entry (morgs) * Reduce telepathy code based on improved PS channel creation API (morgs) * Open URLs via show_object_in_journal (morgs) * Update pippy metadata based on Pippy (morgs) * Updated translations (pootle) 35 * #6066: Make web links copied to clipboard, pasteable in Write, Browse, Terminal (morgs) * Added AUTHORS, COPYING (morgs) * Updated translations (pootle) 34 * Updated translations: ur, bn (pootle) * #2351: Scrolling fixed (marcopg) 33 32 * #5542: Repackaged as a Pippy application. (cscott) 31 * Updated translations: fa, is (pootle) * #5080: Copy to clipboard with targets (morgs) 30 * Updated translations: es, fr, ne, pt, ro, ru, ur (pootle) * #5160: Chat should not autoscroll while you scroll up (morgs) 29 * #5080: add a "copy to clipboard" palette for URL's (cassidy) * Updated translations: fr, es, el, de, ar, zh_TW, it, nl, pt_BR (pootle) 28 * use NotifyAlert from sugar.graphics.alert instead of local copy (thanks erikos!) (morgs) 27 * Use sugar.graphics.alert to show status info (morgs) * #4320: better URL handling and display (morgs) * #4331: Don't crash/ignore non-Sugar buddies (morgs) 26 * #4320 Better URL support (morgs) 25 * #3417 Resuming shows chat history (morgs) * self.set_title() considered harmful (morgs) * New UI look per Eben's mockups (morgs) 24 * #3556: Updated spanish translation (morgs) 23 * Updated spanish translation (morgs) 22 * Revert message dialog added by mistake. (marco) 21 * Add spanish translation (xavi) 20 * Update translation strings - genpot (morgs) * #3248 Make chat not shared by default (morgs) 19 * Added missing fill_color in icon (erikos) 18 * New activity icon, Fix for #2829 (erikos) 16 * Fix icon and roundbox changes in sugar (morgs) * Add greek translation (simosx) * Add arabic translation (khaled) 15 * Rename buddy icon (morgs) * Regen Chat.pot (danw) * French translation (marcopg) 14 * #2714 sugar.graphics cleanup (morgs) * #2578 German translation (morgs) 13 * Added gettext for i18n (morgs) 12 * #2347 Set initial focus on text entry (cassidy) 11 * #2356 Basic link support. (marco) 10 * Adapt to sugar API change (marco) 9 * Fix buddy handles for Salut (Link Local) channels (morgs) * Show status messages in different colour to text messages (morgs) 8 * Use room provided by PS instead of hardcoded global room (morgs) """ def pippy_activity_version(): """Returns the version number of the generated activity bundle.""" return 60 def pippy_activity_news(): """Return the NEWS file for this activity.""" return CHAT_NEWS def pippy_activity_icon(): """Return an SVG document specifying the icon for this activity.""" return CHAT_ICON def pippy_activity_class(): """Return the class which should be started to run this activity.""" return 'pippy_app.Chat' def pippy_activity_extra_info(): return "host_version = 1" if False: # only the official Chat should have this bundle_id. def pippy_activity_bundle_id(): """Return the bundle_id for the generated activity.""" return 'org.laptop.Chat' if __name__ == '__main__': print "Use 'Keep As Activity' to create a new version of Chat."