diff --git a/src/chat_control.py b/src/chat_control.py index 0b61cbf8f..e5267bd26 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -343,13 +343,13 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): if resource is None: # We very likely got a contact with a random resource. # This is bad, we need the highest for caps etc. - c = gajim.contacts.get_contact_with_highest_priority( - acct, contact.jid) + c = gajim.contacts.get_contact_with_highest_priority(acct, + contact.jid) if c and not isinstance(c, GC_Contact): contact = c MessageControl.__init__(self, type_id, parent_win, widget_name, - contact, acct, resource=resource) + contact, acct, resource=resource) widget = self.xml.get_object('history_button') # set document-open-recent icon for history button @@ -371,85 +371,90 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): # Create banner and connect signals widget = self.xml.get_object('banner_eventbox') id_ = widget.connect('button-press-event', - self._on_banner_eventbox_button_press_event) + self._on_banner_eventbox_button_press_event) self.handlers[id_] = widget self.urlfinder = re.compile( - r"(www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'\"]+[^!,\.\s<>\)'\"\]]") + r"(www\.(?!\.)|[a-z][a-z0-9+.-]*://)[^\s<>'\"]+[^!,\.\s<>\)'\"\]]") self.banner_status_label = self.xml.get_object('banner_label') id_ = self.banner_status_label.connect('populate_popup', - self.on_banner_label_populate_popup) + self.on_banner_label_populate_popup) self.handlers[id_] = self.banner_status_label # Init DND self.TARGET_TYPE_URI_LIST = 80 self.dnd_list = [('text/uri-list', 0, self.TARGET_TYPE_URI_LIST), - ('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_APP, 0)] + ('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_APP, 0)] id_ = self.widget.connect('drag_data_received', - self._on_drag_data_received) + self._on_drag_data_received) self.handlers[id_] = self.widget self.widget.drag_dest_set(gtk.DEST_DEFAULT_MOTION | - gtk.DEST_DEFAULT_HIGHLIGHT | - gtk.DEST_DEFAULT_DROP, - self.dnd_list, gtk.gdk.ACTION_COPY) + gtk.DEST_DEFAULT_HIGHLIGHT | + gtk.DEST_DEFAULT_DROP, + self.dnd_list, gtk.gdk.ACTION_COPY) # Create textviews and connect signals self.conv_textview = ConversationTextview(self.account) id_ = self.conv_textview.connect('quote', self.on_quote) self.handlers[id_] = self.conv_textview.tv id_ = self.conv_textview.tv.connect('key_press_event', - self._conv_textview_key_press_event) + self._conv_textview_key_press_event) self.handlers[id_] = self.conv_textview.tv # FIXME: DND on non editable TextView, find a better way self.drag_entered = False id_ = self.conv_textview.tv.connect('drag_data_received', - self._on_drag_data_received) + self._on_drag_data_received) self.handlers[id_] = self.conv_textview.tv id_ = self.conv_textview.tv.connect('drag_motion', self._on_drag_motion) self.handlers[id_] = self.conv_textview.tv id_ = self.conv_textview.tv.connect('drag_leave', self._on_drag_leave) self.handlers[id_] = self.conv_textview.tv self.conv_textview.tv.drag_dest_set(gtk.DEST_DEFAULT_MOTION | - gtk.DEST_DEFAULT_HIGHLIGHT | - gtk.DEST_DEFAULT_DROP, - self.dnd_list, gtk.gdk.ACTION_COPY) + gtk.DEST_DEFAULT_HIGHLIGHT | + gtk.DEST_DEFAULT_DROP, + self.dnd_list, gtk.gdk.ACTION_COPY) self.conv_scrolledwindow = self.xml.get_object( - 'conversation_scrolledwindow') + 'conversation_scrolledwindow') self.conv_scrolledwindow.add(self.conv_textview.tv) widget = self.conv_scrolledwindow.get_vadjustment() id_ = widget.connect('value-changed', - self.on_conversation_vadjustment_value_changed) + self.on_conversation_vadjustment_value_changed) self.handlers[id_] = widget id_ = widget.connect('changed', - self.on_conversation_vadjustment_changed) + self.on_conversation_vadjustment_changed) self.handlers[id_] = widget self.scroll_to_end_id = None self.was_at_the_end = True + self.correcting = False + self.last_sent_msg = None + self.last_sent_txt = None + self.last_received_txt = {} # one per name + self.last_received_id = {} # one per name # add MessageTextView to UI and connect signals self.msg_scrolledwindow = self.xml.get_object('message_scrolledwindow') self.msg_textview = MessageTextView() id_ = self.msg_textview.connect('mykeypress', - self._on_message_textview_mykeypress_event) + self._on_message_textview_mykeypress_event) self.handlers[id_] = self.msg_textview self.msg_scrolledwindow.add(self.msg_textview) id_ = self.msg_textview.connect('key_press_event', - self._on_message_textview_key_press_event) + self._on_message_textview_key_press_event) self.handlers[id_] = self.msg_textview id_ = self.msg_textview.connect('size-request', self.size_request) self.handlers[id_] = self.msg_textview id_ = self.msg_textview.connect('populate_popup', - self.on_msg_textview_populate_popup) + self.on_msg_textview_populate_popup) self.handlers[id_] = self.msg_textview # Setup DND id_ = self.msg_textview.connect('drag_data_received', - self._on_drag_data_received) + self._on_drag_data_received) self.handlers[id_] = self.msg_textview self.msg_textview.drag_dest_set(gtk.DEST_DEFAULT_MOTION | - gtk.DEST_DEFAULT_HIGHLIGHT, - self.dnd_list, gtk.gdk.ACTION_COPY) + gtk.DEST_DEFAULT_HIGHLIGHT, + self.dnd_list, gtk.gdk.ACTION_COPY) self.update_font() @@ -474,7 +479,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): # (so toggle works ok) img = self.xml.get_object('emoticons_button_image') img.set_from_file(os.path.join(gajim.DATA_DIR, 'emoticons', 'static', - 'smile.png')) + 'smile.png')) self.toggle_emoticons() # Attach speller @@ -893,12 +898,23 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): label = self.get_seclabel() + def _cb(msg, cb, *cb_args): + self.last_sent_msg = msg + self.last_sent_txt = cb_args[1] + if cb: + cb(msg, *cb_args) + + if self.correcting and self.last_sent_msg: + correction_msg = self.last_sent_msg + else: + correction_msg = None + gajim.nec.push_outgoing_event(MessageOutgoingEvent(None, account=self.account, jid=self.contact.jid, message=message, keyID=keyID, type_=type_, chatstate=chatstate, msg_id=msg_id, resource=resource, user_nick=self.user_nick, xhtml=xhtml, - label=label, callback=callback, callback_args=callback_args, - control=self, attention=attention)) + label=label, callback=_cb, callback_args=[callback] + callback_args, + control=self, attention=attention, correction_msg=correction_msg)) # Record the history of sent messages self.save_message(message, 'sent') @@ -939,9 +955,11 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): def print_conversation_line(self, text, kind, name, tim, other_tags_for_name=[], other_tags_for_time=[], other_tags_for_text=[], count_as_new=True, subject=None, old_kind=None, xhtml=None, simple=False, - xep0184_id=None, graphics=True, displaymarking=None, msg_id=None): + xep0184_id=None, graphics=True, displaymarking=None, msg_id=None, + correct_id=None): """ Print 'chat' type messages + correct_id = (message_id, correct_id) """ jid = self.contact.jid full_jid = self.get_full_jid() @@ -949,16 +967,28 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): end = False if self.was_at_the_end or kind == 'outgoing': end = True - textview.print_conversation_line(text, jid, kind, name, tim, - other_tags_for_name, other_tags_for_time, other_tags_for_text, - subject, old_kind, xhtml, simple=simple, graphics=graphics, - displaymarking=displaymarking) + old_txt = '' + if name in self.last_received_txt: + old_txt = self.last_received_txt[name] + if correct_id and correct_id[1] and \ + name in self.conv_textview.last_received_message_marks and \ + correct_id[1] == self.last_received_id[name]: + self.conv_textview.correct_last_received_message(text, xhtml, + name, old_txt) + else: + textview.print_conversation_line(text, jid, kind, name, tim, + other_tags_for_name, other_tags_for_time, other_tags_for_text, + subject, old_kind, xhtml, simple=simple, graphics=graphics, + displaymarking=displaymarking) if xep0184_id is not None: textview.show_xep0184_warning(xep0184_id) if not count_as_new: return + if kind in ('incoming', 'outgoing'): + self.last_received_txt[name] = text + self.last_received_id[name] = correct_id[0] if kind == 'incoming': if not self.type_id == message_control.TYPE_GC or \ gajim.config.get('notify_on_all_muc_messages') or \ @@ -1370,7 +1400,21 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): start_iter = msg_buf.get_start_iter() end_iter = msg_buf.get_end_iter() self.orig_msg = msg_buf.get_text(start_iter, end_iter, 0).decode( - 'utf-8') + 'utf-8') + if pos == size and size > 0 and direction == 'up' and \ + msg_type == 'sent' and not self.correcting: + self.correcting = True + self.old_message_tv_color = self.msg_textview.get_style().base[0] + self.msg_textview.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse( + 'PaleGoldenrod')) + message = history[pos - 1] + msg_buf.set_text(message) + return + if self.correcting: + # We were previously correcting + self.msg_textview.modify_base(gtk.STATE_NORMAL, + self.old_message_tv_color) + self.correcting = False pos += -1 if direction == 'up' else +1 if pos == -1: return @@ -1456,6 +1500,8 @@ class ChatControl(ChatControlBase): 'chat_control', contact, acct, resource) self.gpg_is_active = False + self.last_recv_message_id = None + self.last_recv_message_marks = None # for muc use: # widget = self.xml.get_object('muc_window_actions_button') self.actions_button = self.xml.get_object('message_window_actions_button') @@ -2318,7 +2364,8 @@ class ChatControl(ChatControlBase): gobject.source_remove(self.possible_inactive_timeout_id) self._schedule_activity_timers() - def _on_sent(id_, contact, message, encrypted, xhtml, label): + def _on_sent(msg, contact, message, encrypted, xhtml, label, old_txt): + id_ = msg.getID() if contact.supports(NS_RECEIPTS) and gajim.config.get_per('accounts', self.account, 'request_receipt'): xep0184_id = id_ @@ -2328,13 +2375,22 @@ class ChatControl(ChatControlBase): displaymarking = label.getTag('displaymarking') else: displaymarking = None - self.print_conversation(message, self.contact.jid, encrypted=encrypted, - xep0184_id=xep0184_id, xhtml=xhtml, displaymarking=displaymarking) + if self.correcting and \ + self.conv_textview.last_sent_message_marks[0]: + self.conv_textview.correct_last_sent_message(message, xhtml, + self.get_our_nick(), old_txt) + self.correcting = False + self.msg_textview.modify_base(gtk.STATE_NORMAL, + self.old_message_tv_color) + return + self.print_conversation(message, self.contact.jid, + encrypted=encrypted, xep0184_id=xep0184_id, xhtml=xhtml, + displaymarking=displaymarking) ChatControlBase.send_message(self, message, keyID, type_='chat', chatstate=chatstate_to_send, xhtml=xhtml, callback=_on_sent, callback_args=[contact, message, encrypted, xhtml, - self.get_seclabel()], process_commands=process_commands, + self.get_seclabel(), self.last_sent_txt], process_commands=process_commands, attention=attention) def check_for_possible_paused_chatstate(self, arg): @@ -2446,7 +2502,7 @@ class ChatControl(ChatControlBase): def print_conversation(self, text, frm='', tim=None, encrypted=False, subject=None, xhtml=None, simple=False, xep0184_id=None, - displaymarking=None, msg_id=None): + displaymarking=None, msg_id=None, correct_id=None): """ Print a line in the conversation @@ -2511,7 +2567,7 @@ class ChatControl(ChatControlBase): ChatControlBase.print_conversation_line(self, text, kind, name, tim, subject=subject, old_kind=self.old_msg_kind, xhtml=xhtml, simple=simple, xep0184_id=xep0184_id, displaymarking=displaymarking, - msg_id=msg_id) + msg_id=msg_id, correct_id=correct_id) if text.startswith('/me ') or text.startswith('/me\n'): self.old_msg_kind = None else: diff --git a/src/common/connection.py b/src/common/connection.py index 376d04e93..978e601a2 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -254,9 +254,11 @@ class CommonConnection: def _prepare_message(self, jid, msg, keyID, type_='chat', subject='', chatstate=None, msg_id=None, resource=None, user_nick=None, xhtml=None, session=None, forward_from=None, form_node=None, label=None, - original_message=None, delayed=None, attention=False, callback=None): + original_message=None, delayed=None, attention=False, correction_msg=None, + callback=None): if not self.connection or self.connected < 2: return 1 + try: jid = self.check_jid(jid) except helpers.InvalidFormat: @@ -306,7 +308,7 @@ class CommonConnection: jid, xhtml, subject, chatstate, msg_id, label, forward_from, delayed, session, form_node, user_nick, keyID, attention, - callback) + correction_msg, callback) gajim.nec.push_incoming_event(GPGTrustKeyEvent(None, conn=self, callback=_on_always_trust)) else: @@ -314,7 +316,7 @@ class CommonConnection: original_message, fjid, resource, jid, xhtml, subject, chatstate, msg_id, label, forward_from, delayed, session, form_node, user_nick, keyID, - attention, callback) + attention, correction_msg, callback) gajim.thread_interface(encrypt_thread, [msg, keyID, False], _on_encrypted, []) return @@ -322,18 +324,19 @@ class CommonConnection: self._message_encrypted_cb(('', error), type_, msg, msgtxt, original_message, fjid, resource, jid, xhtml, subject, chatstate, msg_id, label, forward_from, delayed, session, - form_node, user_nick, keyID, attention, callback) + form_node, user_nick, keyID, attention, correction_msg, + callback) return self._on_continue_message(type_, msg, msgtxt, original_message, fjid, resource, jid, xhtml, subject, msgenc, keyID, chatstate, msg_id, label, forward_from, delayed, session, form_node, user_nick, - attention, callback) + attention, correction_msg, callback) def _message_encrypted_cb(self, output, type_, msg, msgtxt, original_message, fjid, resource, jid, xhtml, subject, chatstate, msg_id, label, forward_from, delayed, session, form_node, user_nick, keyID, - attention, callback): + attention, correction_msg, callback): msgenc, error = output if msgenc and not error: @@ -346,7 +349,7 @@ class CommonConnection: self._on_continue_message(type_, msg, msgtxt, original_message, fjid, resource, jid, xhtml, subject, msgenc, keyID, chatstate, msg_id, label, forward_from, delayed, session, - form_node, user_nick, attention, callback) + form_node, user_nick, attention, correction_msg, callback) return # Encryption failed, do not send message tim = localtime() @@ -356,7 +359,32 @@ class CommonConnection: def _on_continue_message(self, type_, msg, msgtxt, original_message, fjid, resource, jid, xhtml, subject, msgenc, keyID, chatstate, msg_id, label, forward_from, delayed, session, form_node, user_nick, attention, - callback): + correction_msg, callback): + + if correction_msg: + id_ = correction_msg.getID() + if correction_msg.getTag('replace'): + correction_msg.delChild('replace') + correction_msg.setTag('replace', attrs={'id': id_}, + namespace=nbxmpp.NS_CORRECT) + id2 = self.connection.getAnID() + correction_msg.setID(id2) + correction_msg.setBody(msgtxt) + if xhtml: + correction_msg.setXHTML(xhtml) + + if session: + session.last_send = time.time() + + # XEP-0200 + if session.enable_encryption: + correction_msg = session.encrypt_stanza(correction_msg) + + if callback: + callback(jid, msg, keyID, forward_from, session, original_message, + subject, type_, correction_msg, xhtml) + return + if type_ == 'chat': msg_iq = nbxmpp.Message(to=fjid, body=msgtxt, typ=type_, xhtml=xhtml) @@ -1941,8 +1969,8 @@ class Connection(CommonConnection, ConnectionHandlers): def send_message(self, jid, msg, keyID=None, type_='chat', subject='', chatstate=None, msg_id=None, resource=None, user_nick=None, xhtml=None, label=None, session=None, forward_from=None, form_node=None, - original_message=None, delayed=None, attention=False, callback=None, - callback_args=[], now=False): + original_message=None, delayed=None, attention=False, correction_msg=None, + callback=None, callback_args=[], now=False): def cb(jid, msg, keyID, forward_from, session, original_message, subject, type_, msg_iq, xhtml): @@ -1961,7 +1989,7 @@ class Connection(CommonConnection, ConnectionHandlers): user_nick=user_nick, xhtml=xhtml, label=label, session=session, forward_from=forward_from, form_node=form_node, original_message=original_message, delayed=delayed, - attention=attention, callback=cb) + attention=attention, correction_msg=correction_msg, callback=cb) def _nec_message_outgoing(self, obj): if obj.account != self.name: @@ -1974,7 +2002,7 @@ class Connection(CommonConnection, ConnectionHandlers): gajim.nec.push_incoming_event(MessageSentEvent(None, conn=self, jid=jid, message=msg, keyID=keyID, chatstate=obj.chatstate)) if obj.callback: - obj.callback(msg_id, *obj.callback_args) + obj.callback(msg_iq, *obj.callback_args) if not obj.is_loggable: return @@ -1986,7 +2014,8 @@ class Connection(CommonConnection, ConnectionHandlers): resource=obj.resource, user_nick=obj.user_nick, xhtml=obj.xhtml, label=obj.label, session=obj.session, forward_from=obj.forward_from, form_node=obj.form_node, original_message=obj.original_message, - delayed=obj.delayed, attention=obj.attention, callback=cb) + delayed=obj.delayed, attention=obj.attention, + correction_msg=obj.correction_msg, callback=cb) def send_contacts(self, contacts, fjid, type_='message'): """ @@ -2524,18 +2553,38 @@ class Connection(CommonConnection, ConnectionHandlers): t.setTagData('password', password) self.connection.send(p) - def send_gc_message(self, jid, msg, xhtml=None, label=None): + def send_gc_message(self, jid, msg, xhtml=None, label=None, + correction_msg=None, callback=None): if not gajim.account_is_connected(self.name): return + if correction_msg: + id_ = correction_msg.getID() + if correction_msg.getTag('replace'): + correction_msg.delChild('replace') + correction_msg.setTag('replace', attrs={'id': id_}, + namespace=nbxmpp.NS_CORRECT) + id2 = self.connection.getAnID() + correction_msg.setID(id2) + correction_msg.setBody(msg) + if xhtml: + correction_msg.setXHTML(xhtml) + self.connection.send(correction_msg) + gajim.nec.push_incoming_event(MessageSentEvent(None, conn=self, + jid=jid, message=msg, keyID=None, chatstate=None)) + if callback: + callback(correction_msg, msg) + return if not xhtml and gajim.config.get('rst_formatting_outgoing_messages'): from common.rst_xhtml_generator import create_xhtml xhtml = create_xhtml(msg) msg_iq = nbxmpp.Message(jid, msg, typ='groupchat', xhtml=xhtml) if label is not None: - msg_iq.addChild(node = label) + msg_iq.addChild(node=label) self.connection.send(msg_iq) gajim.nec.push_incoming_event(MessageSentEvent(None, conn=self, jid=jid, message=msg, keyID=None, chatstate=None)) + if callback: + callback(msg_iq, msg) def send_gc_subject(self, jid, subject): if not gajim.account_is_connected(self.name): diff --git a/src/common/connection_handlers_events.py b/src/common/connection_handlers_events.py index d847fc6f7..d730fd16c 100644 --- a/src/common/connection_handlers_events.py +++ b/src/common/connection_handlers_events.py @@ -1250,6 +1250,7 @@ class DecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): self.popup = False self.msg_id = None # id in log database self.attention = False # XEP-0224 + self.correct_id = None # XEP-0308 self.receipt_request_tag = self.stanza.getTag('request', namespace=nbxmpp.NS_RECEIPTS) @@ -1293,6 +1294,10 @@ class DecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): self.msgtxt += _('URL:') self.msgtxt += ' ' + self.oob_url + replace = self.stanza.getTag('replace', namespace=nbxmpp.NS_CORRECT) + if replace: + self.correct_id = replace.getAttr('id') + return True class ChatstateReceivedEvent(nec.NetworkIncomingEvent): @@ -1320,6 +1325,7 @@ class GcMessageReceivedEvent(nec.NetworkIncomingEvent): self.nickname = self.msg_obj.resource self.timestamp = self.msg_obj.timestamp self.xhtml_msgtxt = self.stanza.getXHTML() + self.correct_id = None # XEP-0308 if gajim.config.get('ignore_incoming_xhtml'): self.xhtml_msgtxt = None @@ -1392,6 +1398,10 @@ class GcMessageReceivedEvent(nec.NetworkIncomingEvent): [self.stanza, self.msg_obj], 0) return + replace = self.stanza.getTag('replace', namespace=nbxmpp.NS_CORRECT) + if replace: + self.correct_id = replace.getAttr('id') + return True class GcSubjectReceivedEvent(nec.NetworkIncomingEvent): @@ -2426,6 +2436,7 @@ class MessageOutgoingEvent(nec.NetworkOutgoingEvent): self.is_loggable = True self.control = None self.attention = False + self.correction_msg = None def generate(self): return True diff --git a/src/common/gajim.py b/src/common/gajim.py index 5b4bca977..d4fa9ade7 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -212,7 +212,7 @@ gajim_common_features = [nbxmpp.NS_BYTESTREAM, nbxmpp.NS_SI, nbxmpp.NS_FILE, nbxmpp.NS_SSN, nbxmpp.NS_MOOD, nbxmpp.NS_ACTIVITY, nbxmpp.NS_NICK, nbxmpp.NS_ROSTERX, nbxmpp.NS_SECLABEL, nbxmpp.NS_HASHES, nbxmpp.NS_HASHES_MD5, nbxmpp.NS_HASHES_SHA1, nbxmpp.NS_HASHES_SHA256, - nbxmpp.NS_HASHES_SHA512] + nbxmpp.NS_HASHES_SHA512, nbxmpp.NS_CORRECT] # Optional features gajim supports per account gajim_optional_features = {} diff --git a/src/conversation_textview.py b/src/conversation_textview.py index fcc8776df..e606545a3 100644 --- a/src/conversation_textview.py +++ b/src/conversation_textview.py @@ -74,7 +74,7 @@ class TextViewImage(gtk.Image): self._disconnect_funcs = [] self.connect('parent-set', self.on_parent_set) self.connect('expose-event', self.on_expose) - self.set_tooltip_text(text) + self.set_tooltip_markup(text) self.anchor.set_data('plaintext', text) def _get_selected(self): @@ -171,7 +171,9 @@ class ConversationTextview(gobject.GObject): FOCUS_OUT_LINE_PIXBUF = gtkgui_helpers.get_icon_pixmap('gajim-muc_separator') XEP0184_WARNING_PIXBUF = gtkgui_helpers.get_icon_pixmap( - 'gajim-receipt_missing') + 'gajim-receipt_missing') + MESSAGE_CORRECTED_PIXBUF = gtkgui_helpers.get_icon_pixmap( + 'gajim-message_corrected') # smooth scroll constants MAX_SCROLL_TIME = 0.4 # seconds @@ -205,6 +207,9 @@ class ConversationTextview(gobject.GObject): self.image_cache = {} self.xep0184_marks = {} self.xep0184_shown = {} + self.last_sent_message_marks = [None, None] + # A pair per occupant. Key is '' in normal chat + self.last_received_message_marks = {} # It's True when we scroll in the code, so we can detect scroll from user self.auto_scrolling = False @@ -459,6 +464,51 @@ class ConversationTextview(gobject.GObject): self.smooth_id = None self.smooth_scroll_timer.cancel() + def show_corrected_message_warning(self, iter_, text=''): + buffer_ = self.tv.get_buffer() + buffer_.begin_user_action() + buffer_.insert(iter_, ' ') + anchor = buffer_.create_child_anchor(iter_) + img = TextViewImage(anchor, text) + img.set_from_pixbuf(ConversationTextview.MESSAGE_CORRECTED_PIXBUF) + img.show() + self.tv.add_child_at_anchor(img, anchor) + buffer_.end_user_action() + + def correct_last_sent_message(self, message, xhtml, name, old_txt): + m1 = self.last_sent_message_marks[0] + m2 = self.last_sent_message_marks[1] + buffer_ = self.tv.get_buffer() + i1 = buffer_.get_iter_at_mark(m1) + i2 = buffer_.get_iter_at_mark(m2) + txt = buffer_.get_text(i1, i2) + buffer_.delete(i1, i2) + i2 = self.print_real_text(message, text_tags=['outgoingtxt'], name=name, + xhtml=xhtml, iter_=i1) + tt_txt = _('Message was corrected. Last message was:\n %s') % \ + old_txt + self.show_corrected_message_warning(i2, tt_txt) + self.last_sent_message_marks[1] = buffer_.create_mark(None, i2, + left_gravity=True) + + def correct_last_received_message(self, message, xhtml, name, old_txt): + if name not in self.last_received_message_marks: + return + m1 = self.last_received_message_marks[name][0] + m2 = self.last_received_message_marks[name][1] + buffer_ = self.tv.get_buffer() + i1 = buffer_.get_iter_at_mark(m1) + i2 = buffer_.get_iter_at_mark(m2) + txt = buffer_.get_text(i1, i2) + buffer_.delete(i1, i2) + i2 = self.print_real_text(message, text_tags=['incomingtxt'], name=name, + xhtml=xhtml, iter_=i1) + tt_txt = _('Message was corrected. Last message was:\n %s') % \ + old_txt + self.show_corrected_message_warning(i2, tt_txt) + self.last_received_message_marks[name][1] = buffer_.create_mark(None, i2, + left_gravity=True) + def show_xep0184_warning(self, id_): if id_ in self.xep0184_marks: return @@ -985,7 +1035,8 @@ class ConversationTextview(gobject.GObject): else: helpers.launch_browser_mailer(kind, word) - def detect_and_print_special_text(self, otext, other_tags, graphics=True): + def detect_and_print_special_text(self, otext, other_tags, graphics=True, + iter_=None): """ Detect special text (emots & links & formatting), print normal text before any special text it founds, then print special text (that happens @@ -1015,6 +1066,10 @@ class ConversationTextview(gobject.GObject): iterator = gajim.interface.emot_and_basic_re.finditer(otext) else: # search for just urls + mail + formatting iterator = gajim.interface.basic_pattern_re.finditer(otext) + if iter_: + end_iter = iter_ + else: + end_iter = buffer_.get_end_iter() for match in iterator: start, end = match.span() special_text = otext[start:end] @@ -1026,18 +1081,19 @@ class ConversationTextview(gobject.GObject): index = end # update index # now print it - self.print_special_text(special_text, other_tags, graphics=graphics) + self.print_special_text(special_text, other_tags, graphics=graphics, + iter_=end_iter) specials_limit -= 1 if specials_limit <= 0: break # add the rest of text located in the index and after - end_iter = buffer_.get_end_iter() insert_tags_func(end_iter, otext[index:], *other_tags) - return buffer_.get_end_iter() + return end_iter - def print_special_text(self, special_text, other_tags, graphics=True): + def print_special_text(self, special_text, other_tags, graphics=True, + iter_=None): """ Is called by detect_and_print_special_text and prints special text (emots, links, formatting) @@ -1056,7 +1112,7 @@ class ConversationTextview(gobject.GObject): text_is_valid_uri = False is_xhtml_link = None show_ascii_formatting_chars = \ - gajim.config.get('show_ascii_formatting_chars') + gajim.config.get('show_ascii_formatting_chars') buffer_ = self.tv.get_buffer() # Detect XHTML-IM link @@ -1074,17 +1130,20 @@ class ConversationTextview(gobject.GObject): text_is_valid_uri = True possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS + if iter_: + end_iter = iter_ + else: + end_iter = buffer_.get_end_iter() if gajim.config.get('emoticons_theme') and \ possible_emot_ascii_caps in gajim.interface.emoticons.keys() and graphics: # it's an emoticon emot_ascii = possible_emot_ascii_caps - end_iter = buffer_.get_end_iter() anchor = buffer_.create_child_anchor(end_iter) img = TextViewImage(anchor, special_text) animations = gajim.interface.emoticons_animations if not emot_ascii in animations: animations[emot_ascii] = gtk.gdk.PixbufAnimation( - gajim.interface.emoticons[emot_ascii]) + gajim.interface.emoticons[emot_ascii]) img.set_from_animation(animations[emot_ascii]) img.show() self.images.append(img) @@ -1095,13 +1154,13 @@ class ConversationTextview(gobject.GObject): text_is_valid_uri and not is_xhtml_link: tags.append('url') elif special_text.startswith('mailto:') and not is_xhtml_link: - tags.append('mail') + tags.append('mail') elif special_text.startswith('xmpp:') and not is_xhtml_link: - tags.append('xmpp') + tags.append('xmpp') elif gajim.interface.sth_at_sth_dot_sth_re.match(special_text) and\ not is_xhtml_link: - # it's a JID or mail - tags.append('sth_at_sth') + # it's a JID or mail + tags.append('sth_at_sth') elif special_text.startswith('*'): # it's a bold text tags.append('bold') if special_text[1] == '/' and special_text[-2] == '/' and\ @@ -1150,7 +1209,6 @@ class ConversationTextview(gobject.GObject): else: # It's nothing special if use_other_tags: - end_iter = buffer_.get_end_iter() insert_tags_func = buffer_.insert_with_tags_by_name if other_tags and isinstance(other_tags[0], gtk.TextTag): insert_tags_func = buffer_.insert_with_tags @@ -1158,7 +1216,6 @@ class ConversationTextview(gobject.GObject): insert_tags_func(end_iter, special_text, *other_tags) if tags: - end_iter = buffer_.get_end_iter() all_tags = tags[:] if use_other_tags: all_tags += other_tags @@ -1168,7 +1225,6 @@ class ConversationTextview(gobject.GObject): if 'url' in tags: puny_text = puny_encode(special_text) if not puny_text.endswith('-'): - end_iter = buffer_.get_end_iter() buffer_.insert(end_iter, " (%s)" % puny_text) def print_empty_line(self): @@ -1178,9 +1234,9 @@ class ConversationTextview(gobject.GObject): self.just_cleared = False def print_conversation_line(self, text, jid, kind, name, tim, - other_tags_for_name=[], other_tags_for_time=[], - other_tags_for_text=[], subject=None, old_kind=None, xhtml=None, - simple=False, graphics=True, displaymarking=None): + other_tags_for_name=[], other_tags_for_time=[], other_tags_for_text=[], + subject=None, old_kind=None, xhtml=None, simple=False, graphics=True, + displaymarking=None): """ Print 'chat' type messages """ @@ -1258,6 +1314,7 @@ class ConversationTextview(gobject.GObject): kind = 'status' other_text_tag = self.detect_other_text_tag(text, kind) text_tags = other_tags_for_text[:] # create a new list + mark1 = None if other_text_tag: # note that color of /me may be overwritten in gc_control text_tags.append(other_text_tag) @@ -1274,11 +1331,21 @@ class ConversationTextview(gobject.GObject): direction_mark=direction_mark) if kind == 'incoming': text_tags.append('incomingtxt') + mark1 = buffer_.create_mark(None, buffer_.get_end_iter(), + left_gravity=True) elif kind == 'outgoing': text_tags.append('outgoingtxt') + mark1 = buffer_.create_mark(None, buffer_.get_end_iter(), + left_gravity=True) self.print_subject(subject) self.print_real_text(text, text_tags, name, xhtml, graphics=graphics) - + if mark1: + mark2 = buffer_.create_mark(None, buffer_.get_end_iter(), + left_gravity=True) + if kind == 'incoming': + self.last_received_message_marks[name] = [mark1, mark2] + elif kind == 'outgoing': + self.last_sent_message_marks = [mark1, mark2] # scroll to the end of the textview if at_the_end or kind == 'outgoing': # we are at the end or we are sending something @@ -1353,16 +1420,19 @@ class ConversationTextview(gobject.GObject): format_ = direction_mark + before_str + name + after_str + ' ' buffer_.insert_with_tags_by_name(end_iter, format_, *name_tags) - def print_subject(self, subject): + def print_subject(self, subject, iter_=None): if subject: # if we have subject, show it too! subject = _('Subject: %s\n') % subject buffer_ = self.tv.get_buffer() - end_iter = buffer_.get_end_iter() + if iter_: + end_iter = iter_ + else: + end_iter = buffer_.get_end_iter() buffer_.insert(end_iter, subject) self.print_empty_line() def print_real_text(self, text, text_tags=[], name=None, xhtml=None, - graphics=True): + graphics=True, iter_=None): """ Add normal and special text. call this to add text """ @@ -1381,4 +1451,5 @@ class ConversationTextview(gobject.GObject): text = '* ' + name + text[3:] text_tags.append('italic') # detect urls formatting and if the user has it on emoticons - self.detect_and_print_special_text(text, text_tags, graphics=graphics) + return self.detect_and_print_special_text(text, text_tags, graphics=graphics, + iter_=iter_) diff --git a/src/groupchat_control.py b/src/groupchat_control.py index deea485ad..64c75adaf 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -1002,9 +1002,26 @@ class GroupchatControl(ChatControlBase): tim=obj.timestamp, xhtml=None, displaymarking=obj.displaymarking) else: + if obj.nick in self.last_received_txt and obj.correct_id and \ + obj.correct_id == self.last_received_id[obj.nick]: + if obj.nick == self.nick: + old_txt = self.last_sent_txt + self.last_sent_txt = obj.msgtxt + self.conv_textview.correct_last_sent_message(obj.msgtxt, + obj.xhtml_msgtxt, obj.nick, old_txt) + else: + old_txt = self.last_received_txt[obj.nick] + self.conv_textview.correct_last_received_message(obj.msgtxt, + obj.xhtml_msgtxt, obj.nick, old_txt) + self.last_received_txt[obj.nick] = obj.msgtxt + self.last_received_id[obj.nick] = obj.stanza.getID() + return + if obj.nick == self.nick: + self.last_sent_txt = obj.msgtxt self.print_conversation(obj.msgtxt, contact=obj.nick, tim=obj.timestamp, xhtml=obj.xhtml_msgtxt, - displaymarking=obj.displaymarking) + displaymarking=obj.displaymarking, + correct_id=(obj.stanza.getID(), None)) obj.needs_highlight = self.needs_visual_notification(obj.msgtxt) def on_private_message(self, nick, msg, tim, xhtml, session, msg_id=None, @@ -1076,7 +1093,7 @@ class GroupchatControl(ChatControlBase): displaymarking=displaymarking) def print_conversation(self, text, contact='', tim=None, xhtml=None, - graphics=True, displaymarking=None): + graphics=True, displaymarking=None, correct_id=None): """ Print a line in the conversation @@ -1139,7 +1156,8 @@ class GroupchatControl(ChatControlBase): ChatControlBase.print_conversation_line(self, text, kind, contact, tim, other_tags_for_name, [], other_tags_for_text, xhtml=xhtml, - graphics=graphics, displaymarking=displaymarking) + graphics=graphics, displaymarking=displaymarking, + correct_id=correct_id) def get_nb_unread(self): type_events = ['printed_marked_gc_msg'] @@ -1580,6 +1598,18 @@ class GroupchatControl(ChatControlBase): else: s = _('%(nick)s is now known as %(new_nick)s') % { 'nick': obj.nick, 'new_nick': obj.new_nick} + tv = self.conv_textview + if obj.nick in tv.last_received_message_marks: + tv.last_received_message_marks[obj.new_nick] = \ + tv.last_received_message_marks[obj.nick] + del tv.last_received_message_marks[obj.nick] + if obj.nick in self.last_received_txt: + self.last_received_txt[obj.new_nick] = \ + self.last_received_txt[obj.nick] + del self.last_received_txt[obj.nick] + self.last_received_id[obj.new_nick] = \ + self.last_received_id[obj.nick] + del self.last_received_id[obj.nick] # We add new nick to muc roster here, so we don't see # that "new_nick has joined the room" when he just changed # nick. @@ -1879,9 +1909,23 @@ class GroupchatControl(ChatControlBase): if message != '' or message != '\n': self.save_message(message, 'sent') + def _cb(msg, msg_txt): + # we'll save sent message text when we'll receive it in + # _nec_gc_message_received + self.last_sent_msg = msg + if self.correcting: + self.correcting = False + self.msg_textview.modify_base(gtk.STATE_NORMAL, + self.old_message_tv_color) + + if self.correcting and self.last_sent_msg: + correction_msg = self.last_sent_msg + else: + correction_msg = None # Send the message gajim.connections[self.account].send_gc_message(self.room_jid, - message, xhtml=xhtml, label=label) + message, xhtml=xhtml, label=label, + correction_msg=correction_msg, callback=_cb) self.msg_textview.get_buffer().set_text('') self.msg_textview.grab_focus() diff --git a/src/roster_window.py b/src/roster_window.py index 92deb602c..3ebd8e5cf 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -2704,7 +2704,7 @@ class RosterWindow: obj.session.control.print_conversation(obj.msgtxt, typ, tim=obj.timestamp, encrypted=obj.encrypted, subject=obj.subject, xhtml=obj.xhtml, displaymarking=obj.displaymarking, - msg_id=obj.msg_id) + msg_id=obj.msg_id, correct_id=(obj.id_, obj.correct_id)) if obj.msg_id: pw = obj.session.control.parent_win end = obj.session.control.was_at_the_end diff --git a/src/session.py b/src/session.py index 8b41756a8..1f083efac 100644 --- a/src/session.py +++ b/src/session.py @@ -99,7 +99,7 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): sectext = _('The database file (%s) cannot be read. Try to ' 'repair it (see http://trac.gajim.org/wiki/DatabaseBackup) ' 'or remove it (all history will be lost).') % \ - common.logger.LOG_DB_PATH + gajim.logger.LOG_DB_PATH gajim.nec.push_incoming_event(InformationEvent(None, conn=self.conn, level='error', pri_txt=pritext, sec_txt=sectext))