Commit 5843ab1f authored by eta's avatar eta

Merge remote-tracking branch 'remotes/eta/master' into master

parents 029dfc0e a224a1d9
Pipeline #6007 failed with stages
in 0 seconds
......@@ -32,5 +32,5 @@ syntax: regexp
^config\.*
^config\/
^scripts\/gajim.*
.#*
!MANIFEST.in
......@@ -129,7 +129,7 @@ class ChatControl(ChatControlBase):
# Menu for the HeaderBar
self.control_menu = gui_menu_builder.get_singlechat_menu(
self.control_id, self.account, self.contact.jid)
self.control_id, self.account, self.contact.jid, self._type)
# Settings menu
self.xml.settings_menu.set_menu_model(self.control_menu)
......@@ -210,8 +210,6 @@ class ChatControl(ChatControlBase):
gui_menu_builder.get_encryption_menu(
self.control_id, self._type, self.account == 'Local'))
self.set_encryption_menu_icon()
# restore previous conversation
self.restore_conversation()
self.msg_textview.grab_focus()
# pylint: disable=line-too-long
......@@ -230,6 +228,7 @@ class ChatControl(ChatControlBase):
('receipt-received', ged.GUI1, self._receipt_received),
('message-error', ged.GUI1, self._on_message_error),
('zeroconf-error', ged.GUI1, self._on_zeroconf_error),
('displayed-received', ged.GUI1, self._displayed_received),
])
if self._type.is_chat:
......@@ -286,6 +285,16 @@ class ChatControl(ChatControlBase):
act.connect('change-state', self._on_send_chatstate)
self.parent_win.window.add_action(act)
value = app.config.get_per(
'contacts', self.contact.jid, 'send_marker', True)
act = Gio.SimpleAction.new_stateful(
'send-marker-' + self.control_id,
None,
GLib.Variant.new_boolean(value))
act.connect('change-state', self._on_send_marker)
self.parent_win.window.add_action(act)
def update_actions(self):
win = self.parent_win.window
online = app.account_is_connected(self.account)
......@@ -433,6 +442,11 @@ class ChatControl(ChatControlBase):
app.config.set_per('contacts', self.contact.jid,
'send_chatstate', param.get_string())
def _on_send_marker(self, action, param):
action.set_state(param)
app.config.set_per('contacts', self.contact.jid,
'send_marker', param.get_boolean())
def subscribe_events(self):
"""
Register listeners to the events class
......@@ -1023,6 +1037,14 @@ class ChatControl(ChatControlBase):
else:
self.old_msg_kind = kind
@event_filter(['account', 'jid'])
def _receipt_received(self, event):
self.conv_textview.show_receipt(event.receipt_id)
@event_filter(['account', 'jid'])
def _displayed_received(self, event):
self.conv_textview.show_displayed(event.marker_id)
def get_tab_label(self):
unread = ''
if self.resource:
......@@ -1196,66 +1218,6 @@ class ChatControl(ChatControlBase):
[self.contact.jid],
[dropped_jid])
def restore_conversation(self):
jid = self.contact.jid
# don't restore lines if it's a transport
if app.jid_is_transport(jid):
return
# number of messages that are in queue and are already logged, we want
# to avoid duplication
pending = len(app.events.get_events(self.account, jid, ['chat', 'pm']))
if self.resource:
pending += len(app.events.get_events(self.account,
self.contact.get_full_jid(),
['chat', 'pm']))
rows = app.logger.get_last_conversation_lines(
self.account, jid, pending)
local_old_kind = None
self.conv_textview.just_cleared = True
for row in rows: # time, kind, message, subject, additional_data
msg = row.message
additional_data = row.additional_data
if not msg: # message is empty, we don't print it
continue
if row.kind in (KindConstant.CHAT_MSG_SENT,
KindConstant.SINGLE_MSG_SENT):
kind = 'outgoing'
name = self.get_our_nick()
elif row.kind in (KindConstant.SINGLE_MSG_RECV,
KindConstant.CHAT_MSG_RECV):
kind = 'incoming'
name = self.contact.get_shown_name()
elif row.kind == KindConstant.ERROR:
kind = 'status'
name = self.contact.get_shown_name()
tim = float(row.time)
if row.subject:
msg = _('Subject: %(subject)s\n%(message)s') % \
{'subject': row.subject, 'message': msg}
ChatControlBase.add_message(self,
msg,
kind,
name,
tim,
restored=True,
old_kind=local_old_kind,
additional_data=additional_data,
message_id=row.message_id,
marker=row.marker,
error=row.error)
if (row.message.startswith('/me ') or
row.message.startswith('/me\n')):
local_old_kind = None
else:
local_old_kind = kind
if rows:
self.conv_textview.print_empty_line()
def read_queue(self):
"""
Read queue and print messages contained in it
......
......@@ -25,6 +25,7 @@
import os
import time
import datetime
import uuid
import tempfile
......@@ -45,6 +46,7 @@ from gajim.common.helpers import AdditionalDataDict
from gajim.common.helpers import event_filter
from gajim.common.contacts import GC_Contact
from gajim.common.const import Chatstate
from gajim.common.const import KindConstant
from gajim.common.structs import OutgoingMessage
from gajim import gtkgui_helpers
......@@ -121,6 +123,10 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
self.control_id = str(uuid.uuid4())
self.session = None
self.fetching_history = False
self.initial_fetch_performed = False
self.last_value = 0
app.last_message_time[self.account][self.get_full_jid()] = 0
self.xml = get_builder('%s.ui' % widget_name)
......@@ -191,11 +197,25 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
self.handlers[id_] = self.conv_textview.tv
self.conv_scrolledwindow = self.xml.conversation_scrolledwindow
self.conv_scrolledwindow.add(self.conv_textview.tv)
self.conv_scrolledwindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS)
self.conv_scrolledwindow.add(self.conv_textview.view)
widget = self.conv_scrolledwindow
id_ = widget.connect('edge-overshot',
self.on_edge_overshot)
self.handlers[id_] = widget
id_ = widget.connect('realize',
self.on_scroll_realize)
self.handlers[id_] = widget
widget = self.conv_scrolledwindow.get_vadjustment()
id_ = widget.connect('changed',
self.on_conversation_vadjustment_changed)
self.handlers[id_] = widget
id_ = widget.connect('value-changed',
self.on_scroll_value_changed)
self.handlers[id_] = widget
id_ = widget.connect('changed',
self.on_scroll_changed)
self.handlers[id_] = widget
vscrollbar = self.conv_scrolledwindow.get_vscrollbar()
id_ = vscrollbar.connect('button-release-event',
......@@ -231,6 +251,9 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
self.received_history_pos = 0
self.orig_msg = None
# For XEP-0333
self.last_msg_id = None
self.set_emoticon_popover()
# Attach speller
......@@ -642,6 +665,72 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
gspell_lang = Gspell.language_get_default()
return gspell_lang
def fetch_n_lines_history(self, n_lines):
if self.fetching_history:
return
ts_end = self.conv_textview.view.first_message_ts
if not ts_end:
ts_end = datetime.datetime.now()
self.fetching_history = True
conversation = app.logger.get_conversation_before(self.account, self.contact.jid, ts_end, n_lines)
adjustment = self.conv_scrolledwindow.get_vadjustment()
# Store these values so we can restore scroll position later
self.old_upper = adjustment.get_upper()
self.old_value = adjustment.get_value()
for msg in conversation:
if not msg:
continue
kind = "status"
contact_name = msg.contact_name
if msg.kind in (KindConstant.SINGLE_MSG_RECV, KindConstant.CHAT_MSG_RECV):
kind = "incoming"
contact_name = self.contact.get_shown_name()
elif msg.kind == KindConstant.GC_MSG:
kind = "incoming"
elif msg.kind in (KindConstant.SINGLE_MSG_SENT, KindConstant.CHAT_MSG_SENT):
kind = "outgoing"
contact_name = self.get_our_nick()
if not msg.message:
continue
self.conv_textview.print_conversation_line(msg.message, kind, contact_name, msg.time,
subject=msg.subject,
additional_data=msg.additional_data,
message_id=msg.message_id,
marker=msg.marker,
history=True)
def on_scroll_realize(self, _widg):
if not self.initial_fetch_performed:
self.initial_fetch_performed = True
self.fetch_n_lines_history(100)
def on_scroll_changed(self, adjustment):
if self.fetching_history:
# Make sure the scroll position is kept
new_upper = adjustment.get_upper()
diff = new_upper - self.old_upper
new_value = diff + self.old_value
adjustment.set_value(new_value)
self.fetching_history = False
return
def on_scroll_value_changed(self, adj):
if self.fetching_history:
return
value = adj.get_value()
last_value = self.last_value
self.last_value = value
if value >= last_value:
return
if value <= int(0.1 * adj.get_upper()):
self.fetch_n_lines_history(10)
def on_edge_overshot(self, scrolledwindow, pos):
if pos != Gtk.PositionType.TOP:
return
self.fetch_n_lines_history(40)
def on_language_changed(self, checker, _param):
gspell_lang = checker.get_language()
per_type = 'contacts'
......@@ -1136,6 +1225,9 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
if restored:
return
if message_id:
self.last_msg_id = message_id
if kind == 'incoming':
if (not self._type.is_groupchat or
self.contact.can_notify() or
......@@ -1181,6 +1273,7 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
event = event_type(text,
subject,
self, msg_log_id,
message_id=message_id,
show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
app.events.add_event(self.account, full_jid, event)
......@@ -1328,6 +1421,12 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
types=type_):
# There were events to remove
self.redraw_after_event_removed(jid)
# XEP-0333 Send <displayed> tag.
con.get_module('ChatMarkers').send_displayed_marker(
self.contact,
self.last_msg_id,
self._type)
self.last_msg_id = None
# send chatstate inactive to the one we're leaving
# and active to the one we visit
if self.msg_textview.has_text():
......@@ -1368,8 +1467,15 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
# we are at the end
if not app.events.remove_events(
self.account, jid, types=types_list):
con = app.connections[self.account]
# There were events to remove
self.redraw_after_event_removed(jid)
# XEP-0333 Send <displayed> tag.
con.get_module('ChatMarkers').send_displayed_marker(
self.contact,
self.last_msg_id,
self._type)
self.last_msg_id = None
def _on_scrollbar_button_release(self, scrollbar, event):
if event.get_button()[1] != 1:
......@@ -1434,6 +1540,8 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
We just removed a 'printed_*' event, redraw contact in roster or
gc_roster and titles in roster and msg_win
"""
if not self.parent_win: # minimized groupchat
return
self.parent_win.redraw_tab(self)
self.parent_win.show_title()
# TODO : get the contact and check get_show_in_roster()
......
......@@ -20,6 +20,7 @@ from nbxmpp.const import StreamError
from nbxmpp.const import ConnectionType
from gi.repository import GLib
from gi.repository import Gtk
from gajim.common import passwords
from gajim.common.nec import NetworkEvent
......@@ -345,6 +346,9 @@ class Client(ConnectionHandlers):
app.nec.push_incoming_event(NetworkEvent('stanza-received',
account=self._account,
stanza=stanza))
while Gtk.events_pending():
Gtk.main_iteration()
def get_own_jid(self):
"""
Return the last full JID we received on a bind event.
......
......@@ -345,6 +345,7 @@ class Config:
'contacts': ({
'speller_language': [opt_str, '', _('Language used for spell checking.')],
'send_chatstate': [opt_str, 'composing_only', _('Chat state notifications that are sent to contacts. Possible values: \'all\', \'composing_only\', \'disabled\'')],
'send_marker': [opt_bool, True, _('If enabled, contacts are informed when you read a message.')],
}, {}),
'encryption': ({
'encryption': [opt_str, '', _('The currently active encryption for that contact.')],
......@@ -357,6 +358,7 @@ class Config:
'minimize_on_autojoin': [opt_bool, True, _('If enabled, the group chat is minimized into the contact list when joining automatically.')],
'minimize_on_close': [opt_bool, True, _('If enabled, the group chat is minimized into the contact list when closing it.')],
'send_chatstate': [opt_str, 'composing_only', _('Chat state notifications that are sent to the group chat. Possible values: \'all\', \'composing_only\' or \'disabled\'.')],
'send_marker': [opt_bool, True, _('If enabled, contacts are informed when you read a message.')],
}, {}),
'plugins': ({
'active': [opt_bool, False, _('If enabled, plugins will be activated on startup (this is saved when exiting Gajim). This option SHOULD NOT be used to (de)activate plugins. Use the plugin window instead.')],
......
......@@ -981,6 +981,7 @@ COMMON_FEATURES = [
Namespace.JINGLE_BYTESTREAM,
Namespace.JINGLE_IBB,
Namespace.AVATAR_METADATA + '+notify',
Namespace.CHATMARKERS
]
......
......@@ -89,13 +89,14 @@ class PmEvent(ChatEvent):
class PrintedChatEvent(Event):
type_ = 'printed_chat'
def __init__(self, message, subject, control, msg_log_id, time_=None,
show_in_roster=False, show_in_systray=True):
message_id=None, show_in_roster=False, show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.message = message
self.subject = subject
self.control = control
self.msg_log_id = msg_log_id
self.message_id = message_id
class PrintedGcMsgEvent(PrintedChatEvent):
type_ = 'printed_gc_msg'
......
......@@ -143,7 +143,7 @@ def timeit(func):
start = time.time()
result = func(self, *args, **kwargs)
exec_time = (time.time() - start) * 1e3
level = 30 if exec_time > 50 else 10
level = 30 if exec_time > 20 else 10
log.log(level, 'Execution time for %s: %s ms',
func.__name__, math.ceil(exec_time))
return result
......@@ -228,6 +228,8 @@ class Logger:
def _connect(*args, **kwargs):
con = sqlite.connect(*args, **kwargs)
con.execute("PRAGMA secure_delete=1")
con.execute("PRAGMA journal_mode = WAL")
con.execute("PRAGMA synchronous = NORMAL")
return con
@classmethod
......@@ -415,7 +417,6 @@ class Logger:
self._con.create_function("like", 1, self._like)
self._con.create_function("get_timeout", 0, self._get_timeout)
self._set_synchronous(False)
try:
self._con.execute("ATTACH DATABASE '%s' AS cache" %
self._cache_db_path.replace("'", "''"))
......@@ -424,16 +425,6 @@ class Logger:
self._con.close()
sys.exit()
@timeit
def _set_synchronous(self, sync):
try:
if sync:
self._con.execute("PRAGMA synchronous = NORMAL")
else:
self._con.execute("PRAGMA synchronous = OFF")
except sqlite.Error:
log.exception('Error')
@staticmethod
def _get_timeout():
"""
......@@ -842,6 +833,35 @@ class Logger:
start_of_day = int(time.mktime(local_time))
return start_of_day
@timeit
def get_conversation_before(self, account, jid, end_ts, n_lines):
"""
Load n_lines lines of conversation with jid before end_ts
:param account: The account
:param jid: The jid for which we request the conversation
:param end_ts: end timestamp / datetime.datetime instance
returns a list of namedtuples
"""
jids = self._get_family_jids(account, jid)
sql = '''
SELECT contact_name, time, kind, show, message, subject,
additional_data, log_line_id, message_id,
marker as "marker [marker]"
FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
AND time < ?
ORDER BY time DESC, log_line_id DESC
LIMIT ?
'''.format(jids=', '.join('?' * len(jids)))
return self._con.execute(sql, tuple(jids) +
(end_ts.timestamp(),
n_lines)).fetchall()
@timeit
def get_conversation_for_date(self, account, jid, date):
"""
......
......@@ -40,6 +40,7 @@ MODULES = [
'bookmarks',
'caps',
'carbons',
'chat_markers',
'chatstates',
'delimiter',
'discovery',
......
# This file is part of Gajim.
#
# Gajim 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; version 3 only.
#
# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
# Chat Markers (XEP-0333)
import nbxmpp
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class ChatMarkers(BaseModule):
_nbxmpp_extends = 'ChatMarkers'
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._process_chat_marker,
ns=nbxmpp.NS_CHATMARKERS,
priority=47),
]
def _process_chat_marker(self, _con, _stanza, properties):
if not properties.is_marker or not properties.marker.is_displayed:
return
if properties.type.is_error:
return
if properties.type.is_groupchat:
manager = self._con.get_module('MUC').get_manager()
muc_data = manager.get(properties.muc_jid)
if muc_data is None:
return
if properties.muc_nickname != muc_data.nick:
return
self._raise_event('read-state-sync', properties)
return
if properties.is_carbon_message and properties.carbon.is_sent:
self._raise_event('read-state-sync', properties)
return
if properties.is_mam_message:
if properties.from_.bareMatch(self._con.get_own_jid()):
return
app.logger.set_marker(app.get_jid_from_account(self._account),
properties.jid,
properties.marker.id,
'displayed')
self._raise_event('displayed-received', properties)
def _raise_event(self, name, properties):
self._log.info('%s: %s %s',
name,
properties.jid,
properties.marker.id)
jid = properties.jid
if not properties.is_muc_pm and not properties.type.is_groupchat:
jid = properties.jid.getBare()
app.nec.push_outgoing_event(
NetworkEvent(name,
account=self._account,
jid=jid,
properties=properties,
type=properties.type,
is_muc_pm=properties.is_muc_pm,
marker_id=properties.marker.id))
def _send_marker(self, contact, marker, id_, type_):
jid = contact.jid
if contact.is_pm_contact:
jid = app.get_jid_without_resource(contact.jid)
config = 'rooms' if type_ in ('gc', 'pm') else 'contacts'
if not app.config.get_per(config, jid, 'send_marker', True):
return
typ = 'groupchat' if type_ == 'gc' else 'chat'
message = nbxmpp.Message(to=contact.jid, typ=typ)
message.setMarker(marker, id_)
self._log.info('Send %s: %s', marker, contact.jid)
self._nbxmpp().send(message)
def send_displayed_marker(self, contact, id_, type_):
self._send_marker(contact, 'displayed', id_, type_)
def get_instance(*args, **kwargs):
return ChatMarkers(*args, **kwargs), 'ChatMarkers'
......@@ -277,8 +277,9 @@ class VCardTemp(BaseModule):
avatar_sha, photo_decoded = self._get_vcard_photo(vcard, request_jid)
if expected_sha != avatar_sha:
self._log.warning('Received: avatar mismatch: %s %s',
request_jid, avatar_sha)
if avatar_sha:
self._log.warning('Received: avatar mismatch: %s %s',
request_jid, avatar_sha)
return
app.interface.save_avatar(photo_decoded)
......
This diff is collapsed.
......@@ -269,6 +269,7 @@ button.flat.link { padding: 0; border: 0; }
/*Receipts*/
.receipt-received-color { color: rgb(75, 181, 67) }
.displayed-received-color { color: rgb(106, 0, 242) }
/*Dataforms*/
.field-fixed { font-size: 16px; font-weight: bold; padding-top:5px;}
......
......@@ -365,6 +365,16 @@ class GroupchatControl(ChatControlBase):
act.connect('change-state', self._on_sync_threshold)
self.parent_win.window.add_action(act)
value = app.config.get_per(
'rooms', self.contact.jid, 'send_marker', False)
act = Gio.SimpleAction.new_stateful(
'send-marker-' + self.control_id,
None,
GLib.Variant.new_boolean(value))
act.connect('change-state', self._on_send_marker)
self.parent_win.window.add_action(act)
def update_actions(self):
if self.parent_win is None:
return
......@@ -630,6 +640,11 @@ class GroupchatControl(ChatControlBase):
app.config.set_per('rooms', self.contact.jid,
'send_chatstate', param.get_string())