Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/gajim.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorwurstsalat <mailtrash@posteo.de>2022-12-03 02:24:15 +0300
committerwurstsalat <mailtrash@posteo.de>2023-06-15 22:55:38 +0300
commit75f57a5cc08af3ca40956c9d23b1effd9331aa02 (patch)
treede5e8a5082c0f49dcce1995c5366362cdf68c9d0
parent025c05bf8a3405131a2d9a67f6a4cf4c6e22af96 (diff)
feat: Add support for XEP-0461: Message Repliesmessage-replies
-rw-r--r--data/gajim.doap7
-rw-r--r--gajim/common/client.py1
-rw-r--r--gajim/common/events.py2
-rw-r--r--gajim/common/modules/message.py17
-rw-r--r--gajim/common/storage/archive.py59
-rw-r--r--gajim/common/storage/base.py17
-rw-r--r--gajim/common/structs.py3
-rw-r--r--gajim/common/util/text.py20
-rw-r--r--gajim/data/gui/message_actions_box.ui333
-rw-r--r--gajim/data/style/gajim.css6
-rw-r--r--gajim/gtk/builder.pyi1
-rw-r--r--gajim/gtk/chat_list_row.py7
-rw-r--r--gajim/gtk/chat_stack.py12
-rw-r--r--gajim/gtk/const.py2
-rw-r--r--gajim/gtk/control.py53
-rw-r--r--gajim/gtk/conversation/rows/message.py21
-rw-r--r--gajim/gtk/conversation/view.py7
-rw-r--r--gajim/gtk/menus.py19
-rw-r--r--gajim/gtk/message_actions_box.py70
-rw-r--r--gajim/gtk/message_input.py10
-rw-r--r--gajim/gtk/referred_message_widget.py93
21 files changed, 576 insertions, 184 deletions
diff --git a/data/gajim.doap b/data/gajim.doap
index aa6c390d3..a2ee6153b 100644
--- a/data/gajim.doap
+++ b/data/gajim.doap
@@ -693,5 +693,12 @@
<xmpp:version>0.1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0461.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.1.0</xmpp:version>
+ </xmpp:SupportedXep>
+ </implements>
</Project>
</rdf:RDF>
diff --git a/gajim/common/client.py b/gajim/common/client.py
index 626e65612..caef18e40 100644
--- a/gajim/common/client.py
+++ b/gajim/common/client.py
@@ -518,6 +518,7 @@ class Client(Observable):
label=message.label,
correct_id=message.correct_id,
message_id=message.message_id,
+ reply_data=message.reply_data,
msg_log_id=log_line_id,
play_sound=message.play_sound))
diff --git a/gajim/common/events.py b/gajim/common/events.py
index b7a6dbb4a..7a09622f2 100644
--- a/gajim/common/events.py
+++ b/gajim/common/events.py
@@ -33,6 +33,7 @@ from nbxmpp.protocol import JID
from nbxmpp.structs import HTTPAuthData
from nbxmpp.structs import LocationData
from nbxmpp.structs import ModerationData
+from nbxmpp.structs import ReplyData
from nbxmpp.structs import RosterItem
from nbxmpp.structs import TuneData
@@ -174,6 +175,7 @@ class MessageSent(ApplicationEvent):
additional_data: AdditionalDataDict
label: SecurityLabel | None
correct_id: str | None
+ reply_data: ReplyData | None
play_sound: bool
diff --git a/gajim/common/modules/message.py b/gajim/common/modules/message.py
index c94214f10..f5b72015a 100644
--- a/gajim/common/modules/message.py
+++ b/gajim/common/modules/message.py
@@ -230,7 +230,8 @@ class Message(BaseModule):
subject=properties.subject,
additional_data=additional_data,
stanza_id=stanza_id or message_id,
- message_id=properties.id)
+ message_id=properties.id,
+ reply_data=properties.reply_data)
event.msg_log_id = msg_log_id
app.ged.raise_event(event)
@@ -279,7 +280,8 @@ class Message(BaseModule):
stanza_id=event.stanza_id,
message_id=event.properties.id,
occupant_id=event.occupant_id,
- real_Jid=event.real_jid)
+ real_Jid=event.real_jid,
+ reply_data=event.properties.reply_data)
def _check_for_mam_compliance(self, room_jid: str, stanza_id: str) -> None:
disco_info = app.storage.cache.get_last_disco_info(room_jid)
@@ -342,6 +344,14 @@ class Message(BaseModule):
stanza.setTag('replace', attrs={'id': message.correct_id},
namespace=Namespace.CORRECT)
+ if message.reply_data is not None:
+ assert message.reply_data.fallback_start is not None
+ assert message.reply_data.fallback_end is not None
+ stanza.setReply(str(message.jid),
+ message.reply_data.id,
+ message.reply_data.fallback_start,
+ message.reply_data.fallback_end)
+
# XEP-0359
message.message_id = generate_id()
stanza.setID(message.message_id)
@@ -430,5 +440,6 @@ class Message(BaseModule):
subject=message.subject,
additional_data=message.additional_data,
message_id=message.message_id,
- stanza_id=message.message_id)
+ stanza_id=message.message_id,
+ reply_data=message.reply_data)
return msg_log_id
diff --git a/gajim/common/storage/archive.py b/gajim/common/storage/archive.py
index 3e52c3a4f..2ec8c41e1 100644
--- a/gajim/common/storage/archive.py
+++ b/gajim/common/storage/archive.py
@@ -38,9 +38,11 @@ from collections.abc import KeysView
from nbxmpp import JID
from nbxmpp.structs import CommonError
from nbxmpp.structs import MessageProperties
+from nbxmpp.structs import ReplyData
from gajim.common import app
from gajim.common import configpaths
+from gajim.common import types
from gajim.common.const import JIDConstant
from gajim.common.const import KindConstant
from gajim.common.const import MAX_MESSAGE_CORRECTION_DELAY
@@ -111,6 +113,7 @@ class ConversationRow(NamedTuple):
stanza_id: str
message_id: str
marker: str
+ reply_data: ReplyData | None
class LastConversationRow(NamedTuple):
@@ -121,6 +124,15 @@ class LastConversationRow(NamedTuple):
additional_data: AdditionalDataDict | None
message_id: str
stanza_id: str
+ reply_data: ReplyData | None
+
+
+class ReferredMessageRow(NamedTuple):
+ log_line_id: int
+ contact_name: str
+ kind: int
+ time: float
+ message: str
class SearchLogRow(NamedTuple):
@@ -268,6 +280,13 @@ class MessageArchiveStorage(SqliteStorage):
]
self._execute_multiple(statements)
+ if user_version < 8:
+ statements = [
+ 'ALTER TABLE logs ADD COLUMN "reply_data" TEXT',
+ 'PRAGMA user_version=8'
+ ]
+ self._execute_multiple(statements)
+
@staticmethod
def _like(search_str: str) -> str:
return f'%{search_str}%'
@@ -394,7 +413,8 @@ class MessageArchiveStorage(SqliteStorage):
additional_data,
stanza_id,
message_id,
- marker as "marker [marker]"
+ marker as "marker [marker]",
+ reply_data as "reply_data [reply_data]"
FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
AND account_id = {account_id}
AND kind NOT IN ({kinds})
@@ -433,7 +453,7 @@ class MessageArchiveStorage(SqliteStorage):
sql = '''
SELECT contact_name, time, kind, message, stanza_id, message_id,
- additional_data
+ additional_data, reply_data as "reply_data [reply_data]"
FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
AND account_id = {account_id}
AND kind NOT IN ({kinds})
@@ -480,7 +500,8 @@ class MessageArchiveStorage(SqliteStorage):
additional_data,
stanza_id,
message_id,
- marker as "marker [marker]"
+ marker as "marker [marker]",
+ reply_data as "reply_data [reply_data]"
FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
AND account_id = {account_id}
AND kind NOT IN ({kinds})
@@ -503,7 +524,8 @@ class MessageArchiveStorage(SqliteStorage):
additional_data,
stanza_id,
message_id,
- marker as "marker [marker]"
+ marker as "marker [marker]",
+ reply_data as "reply_data [reply_data]"
FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
AND account_id = {account_id}
AND kind NOT IN ({kinds})
@@ -557,7 +579,8 @@ class MessageArchiveStorage(SqliteStorage):
additional_data,
stanza_id,
message_id,
- marker as "marker [marker]"
+ marker as "marker [marker]",
+ reply_data as "reply_data [reply_data]"
FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
AND account_id = {account_id}
AND kind NOT IN ({kinds})
@@ -996,7 +1019,7 @@ class MessageArchiveStorage(SqliteStorage):
sql = '''
SELECT contact_name, time, kind, message, stanza_id, message_id,
- additional_data
+ additional_data, reply_data as "reply_data [reply_data]"
FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
AND account_id = {account_id}
AND message_id = ?
@@ -1085,6 +1108,30 @@ class MessageArchiveStorage(SqliteStorage):
return True
@timeit
+ def get_referred_message(self,
+ contact: types.ChatContactT,
+ message_id: str
+ ) -> ReferredMessageRow | None:
+
+ jids = [contact.jid]
+ account_id = self.get_account_id(contact.account)
+ message_id_type = 'message_id'
+ if contact.is_groupchat:
+ message_id_type = 'stanza_id'
+
+ sql = '''
+ SELECT log_line_id, contact_name, kind, time, message
+ FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
+ AND account_id = {account_id}
+ AND {message_id_type} = ?
+ '''.format(jids=', '.join('?' * len(jids)),
+ account_id=account_id,
+ message_id_type=message_id_type)
+ return self._con.execute(
+ sql,
+ tuple(jids) + (message_id, )).fetchone()
+
+ @timeit
def update_additional_data(self,
account: str,
stanza_id: str,
diff --git a/gajim/common/storage/base.py b/gajim/common/storage/base.py
index 107c1b65d..9037a6e1b 100644
--- a/gajim/common/storage/base.py
+++ b/gajim/common/storage/base.py
@@ -37,6 +37,7 @@ from nbxmpp.protocol import Iq
from nbxmpp.protocol import JID
from nbxmpp.structs import CommonError
from nbxmpp.structs import DiscoInfo
+from nbxmpp.structs import ReplyData
from nbxmpp.structs import RosterItem
_T = TypeVar('_T')
@@ -68,6 +69,22 @@ sqlite3.register_converter('common_error', _convert_common_error)
sqlite3.register_adapter(CommonError, _adapt_common_error)
+def _convert_reply_data(reply_data: bytes) -> ReplyData:
+ data = json.loads(reply_data)
+ return ReplyData(to=data['to'],
+ id=data['id'],
+ fallback_start=data['fallback_start'],
+ fallback_end=data['fallback_end'])
+
+
+def _adapt_reply_data(reply_data: ReplyData) -> str:
+ return json.dumps(reply_data._asdict())
+
+
+sqlite3.register_converter('reply_data', _convert_reply_data)
+sqlite3.register_adapter(ReplyData, _adapt_reply_data)
+
+
def _convert_marker(marker: bytes):
return 'received' if int(marker) == 0 else 'displayed'
diff --git a/gajim/common/structs.py b/gajim/common/structs.py
index cc25f1d04..7263c4b12 100644
--- a/gajim/common/structs.py
+++ b/gajim/common/structs.py
@@ -31,6 +31,7 @@ from nbxmpp.modules.security_labels import SecurityLabel
from nbxmpp.protocol import JID
from nbxmpp.structs import MucSubject
from nbxmpp.structs import PresenceProperties
+from nbxmpp.structs import ReplyData
from gajim.common import types
from gajim.common.const import KindConstant
@@ -98,6 +99,7 @@ class OutgoingMessage:
control: Any | None = None,
attention: bool | None = None,
correct_id: str | None = None,
+ reply_data: ReplyData | None = None,
oob_url: str | None = None,
xhtml: str | None = None,
nodes: Any | None = None,
@@ -136,6 +138,7 @@ class OutgoingMessage:
self.control = control
self.attention = attention
self.correct_id = correct_id
+ self.reply_data = reply_data
self.oob_url = oob_url
diff --git a/gajim/common/util/text.py b/gajim/common/util/text.py
index 2ab79dde9..50c16d8ac 100644
--- a/gajim/common/util/text.py
+++ b/gajim/common/util/text.py
@@ -51,6 +51,26 @@ def jid_to_iri(jid: str) -> str:
return 'xmpp:' + escape_iri_path(jid)
+def remove_fallback_text(text: str,
+ start: int | None,
+ end: int | None
+ ) -> str:
+
+ if start is None:
+ start = 0
+
+ if end is None:
+ return text[start:]
+
+ before = text[:start]
+ after = text[end:]
+ return before + after
+
+
+def quote_text(text: str) -> str:
+ return '> ' + text.replace('\n', '\n> ') + '\n'
+
+
def format_duration(ns: float, total_ns: float) -> str:
seconds = ns / 1e9
minutes = seconds / 60
diff --git a/gajim/data/gui/message_actions_box.ui b/gajim/data/gui/message_actions_box.ui
index 987ccb583..0fb0ad85d 100644
--- a/gajim/data/gui/message_actions_box.ui
+++ b/gajim/data/gui/message_actions_box.ui
@@ -5,196 +5,213 @@
<object class="GtkBox" id="box">
<property name="visible">True</property>
<property name="can-focus">False</property>
- <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="hexpand">True</property>
- <property name="spacing">2</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">3</property>
<child>
- <placeholder/>
- </child>
- <child>
- <object class="GtkButton" id="encryption_details_button">
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
+ <object class="GtkBox" id="action_box">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
- <property name="no-show-all">True</property>
- <property name="relief">none</property>
- <signal name="clicked" handler="_on_encryption_details_clicked" swapped="no"/>
+ <property name="hexpand">True</property>
+ <property name="spacing">2</property>
<child>
- <object class="GtkImage" id="encryption_details_image">
- <property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon_size">1</property>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkButton" id="encryption_details_button">
+ <property name="can-focus">True</property>
+ <property name="focus-on-click">False</property>
+ <property name="receives-default">True</property>
+ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+ <property name="no-show-all">True</property>
+ <property name="relief">none</property>
+ <signal name="clicked" handler="_on_encryption_details_clicked" swapped="no"/>
+ <child>
+ <object class="GtkImage" id="encryption_details_image">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ <style>
+ <class name="message-actions-box-button"/>
+ </style>
</object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack-type">end</property>
+ <property name="position">1</property>
+ </packing>
</child>
- <style>
- <class name="message-actions-box-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="pack-type">end</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="encryption_menu_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="relief">none</property>
<child>
- <object class="GtkImage" id="encryption_image">
+ <object class="GtkMenuButton" id="encryption_menu_button">
<property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">channel-insecure-symbolic</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage" id="encryption_image">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">channel-insecure-symbolic</property>
+ </object>
+ </child>
+ <style>
+ <class name="message-actions-box-button"/>
+ </style>
</object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack-type">end</property>
+ <property name="position">2</property>
+ </packing>
</child>
- <style>
- <class name="message-actions-box-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack-type">end</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="sendfile_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="action-name">win.send-file</property>
- <property name="action-target">['']</property>
- <property name="relief">none</property>
<child>
- <object class="GtkImage">
+ <object class="GtkButton" id="sendfile_button">
<property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">mail-attachment-symbolic</property>
+ <property name="can-focus">True</property>
+ <property name="focus-on-click">False</property>
+ <property name="receives-default">True</property>
+ <property name="action-name">win.send-file</property>
+ <property name="action-target">['']</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">mail-attachment-symbolic</property>
+ </object>
+ </child>
+ <style>
+ <class name="message-actions-box-button"/>
+ </style>
</object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack-type">end</property>
+ <property name="position">3</property>
+ </packing>
</child>
- <style>
- <class name="message-actions-box-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="pack-type">end</property>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="emoticons_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="receives-default">True</property>
- <property name="tooltip-text" translatable="yes">Show a list of emojis</property>
- <property name="action-name">win.show-emoji-chooser</property>
- <property name="relief">none</property>
<child>
- <object class="GtkImage">
+ <object class="GtkMenuButton" id="emoticons_button">
<property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">face-smile-symbolic</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="tooltip-text" translatable="yes">Show a list of emojis</property>
+ <property name="action-name">win.show-emoji-chooser</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">face-smile-symbolic</property>
+ </object>
+ </child>
+ <style>
+ <class name="message-actions-box-button"/>
+ </style>
</object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="send_message_button">
+ <property name="can-focus">True</property>
+ <property name="focus-on-click">False</property>
+ <property name="receives-default">True</property>
+ <property name="no-show-all">True</property>
+ <property name="tooltip-text" translatable="yes">Send Message</property>
+ <property name="action-name">win.send-message</property>
+ <property name="relief">none</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">gajim-send-message-symbolic</property>
+ </object>
+ </child>
+ <style>
+ <class name="message-actions-box-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack-type">end</property>
+ <property name="position">6</property>
+ </packing>
</child>
- <style>
- <class name="message-actions-box-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">5</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="send_message_button">
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="no-show-all">True</property>
- <property name="tooltip-text" translatable="yes">Send Message</property>
- <property name="action-name">win.send-message</property>
- <property name="relief">none</property>
<child>
- <object class="GtkImage">
+ <object class="GtkMenuButton" id="formattings_button">
<property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">gajim-send-message-symbolic</property>
+ <property name="can-focus">True</property>
+ <property name="focus-on-click">False</property>
+ <property name="receives-default">True</property>
+ <property name="tooltip-text" translatable="yes">Format your message</property>
+ <property name="relief">none</property>
+ <property name="direction">up</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">format-text-bold-symbolic</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ <style>
+ <class name="message-actions-box-button"/>
+ </style>
</object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">7</property>
+ </packing>
</child>
- <style>
- <class name="message-actions-box-button"/>
- </style>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="pack-type">end</property>
- <property name="position">6</property>
- </packing>
- </child>
- <child>
- <object class="GtkMenuButton" id="formattings_button">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="focus-on-click">False</property>
- <property name="receives-default">True</property>
- <property name="tooltip-text" translatable="yes">Format your message</property>
- <property name="relief">none</property>
- <property name="direction">up</property>
<child>
- <object class="GtkImage">
+ <object class="GtkScrolledWindow" id="input_scrolled">
<property name="visible">True</property>
- <property name="can-focus">False</property>
- <property name="icon-name">format-text-bold-symbolic</property>
- <property name="icon_size">1</property>
+ <property name="can-focus">True</property>
+ <property name="margin-start">3</property>
+ <property name="margin-end">3</property>
+ <property name="hscrollbar-policy">external</property>
+ <property name="shadow-type">in</property>
+ <property name="overlay-scrolling">False</property>
+ <property name="max-content-height">100</property>
+ <property name="propagate-natural-height">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ <style>
+ <class name="message-input-border"/>
+ <class name="scrolled-no-border"/>
+ <class name="no-scroll-indicator"/>
+ <class name="scrollbar-style"/>
+ <class name="one-line-scrollbar"/>
+ </style>
</object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">9</property>
+ </packing>
</child>
- <style>
- <class name="message-actions-box-button"/>
- </style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
- <property name="position">7</property>
+ <property name="pack-type">end</property>
+ <property name="position">0</property>
</packing>
</child>
<child>
- <object class="GtkScrolledWindow" id="input_scrolled">
- <property name="visible">True</property>
- <property name="can-focus">True</property>
- <property name="margin-start">3</property>
- <property name="margin-end">3</property>
- <property name="hscrollbar-policy">external</property>
- <property name="shadow-type">in</property>
- <property name="overlay-scrolling">False</property>
- <property name="max-content-height">100</property>
- <property name="propagate-natural-height">True</property>
- <child>
- <placeholder/>
- </child>
- <style>
- <class name="message-input-border"/>
- <class name="scrolled-no-border"/>
- <class name="no-scroll-indicator"/>
- <class name="scrollbar-style"/>
- <class name="one-line-scrollbar"/>
- </style>
- </object>
- <packing>
- <property name="expand">True</property>
- <property name="fill">True</property>
- <property name="position">9</property>
- </packing>
+ <placeholder/>
</child>
</object>
</interface>
diff --git a/gajim/data/style/gajim.css b/gajim/data/style/gajim.css
index ad50008c5..d9733dc58 100644
--- a/gajim/data/style/gajim.css
+++ b/gajim/data/style/gajim.css
@@ -643,6 +643,12 @@ infobar.error > revealer > box {
padding: 0;
}
+/* ReferredMessageWidget */
+.referred-message {
+ border-left: 3px solid @borders;
+ padding: 3px 8px;
+}
+
/* PreviewWidget */
.preview-stack > box {
border: 1px solid;
diff --git a/gajim/gtk/builder.pyi b/gajim/gtk/builder.pyi
index 3cac99713..2b5e4646f 100644
--- a/gajim/gtk/builder.pyi
+++ b/gajim/gtk/builder.pyi
@@ -592,6 +592,7 @@ class ManageSoundsBuilder(Builder):
class MessageActionsBoxBuilder(Builder):
box: Gtk.Box
+ action_box: Gtk.Box
encryption_details_button: Gtk.Button
encryption_details_image: Gtk.Image
encryption_menu_button: Gtk.MenuButton
diff --git a/gajim/gtk/chat_list_row.py b/gajim/gtk/chat_list_row.py
index 643b3a98a..0d9a39881 100644
--- a/gajim/gtk/chat_list_row.py
+++ b/gajim/gtk/chat_list_row.py
@@ -49,6 +49,7 @@ from gajim.common.preview_helpers import split_geo_uri
from gajim.common.storage.draft import DraftStorage
from gajim.common.types import ChatContactT
from gajim.common.types import OneOnOneContactT
+from gajim.common.util.text import remove_fallback_text
from gajim.gtk.builder import get_builder
from gajim.gtk.menus import get_chat_list_row_menu
@@ -157,6 +158,12 @@ class ChatListRow(Gtk.ListBoxRow):
if line.message is not None:
message_text = line.message
+ if line.reply_data is not None:
+ message_text = remove_fallback_text(
+ line.message,
+ line.reply_data.fallback_start,
+ line.reply_data.fallback_end)
+
if line.additional_data is not None:
retracted_by = line.additional_data.get_value(
'retracted', 'by')
diff --git a/gajim/gtk/chat_stack.py b/gajim/gtk/chat_stack.py
index b2a861643..99988286d 100644
--- a/gajim/gtk/chat_stack.py
+++ b/gajim/gtk/chat_stack.py
@@ -519,6 +519,7 @@ class ChatStack(Gtk.Stack, EventHelper):
app.window.get_action('quote').set_enabled(online)
app.window.get_action('mention').set_enabled(online)
+ app.window.get_action('reply').set_enabled(online)
def _update_group_chat_actions(self, contact: GroupchatContact) -> None:
joined = contact.is_joined
@@ -543,6 +544,7 @@ class ChatStack(Gtk.Stack, EventHelper):
app.window.get_action('quote').set_enabled(joined)
app.window.get_action('mention').set_enabled(joined)
+ app.window.get_action('reply').set_enabled(joined)
app.window.get_action('retract-message').set_enabled(joined)
def _update_participant_actions(self,
@@ -766,6 +768,11 @@ class ChatStack(Gtk.Stack, EventHelper):
if correct_id is None:
return
+ reply_data = None
+ message_reply = self._message_action_box.get_reply_data()
+ if message_reply is not None:
+ reply_data, message = message_reply
+
chatstate = client.get_module('Chatstate').get_active_chatstate(
contact)
@@ -780,11 +787,14 @@ class ChatStack(Gtk.Stack, EventHelper):
chatstate=chatstate,
label=label,
control=self._chat_control,
- correct_id=correct_id)
+ correct_id=correct_id,
+ reply_data=reply_data)
client.send_message(message_)
self._message_action_box.msg_textview.clear()
+ if message_reply is not None:
+ self._message_action_box.disable_reply_mode()
app.storage.drafts.set(contact, '')
def get_last_message_id(self, contact: ChatContactT) -> str | None:
diff --git a/gajim/gtk/const.py b/gajim/gtk/const.py
index 107a910df..3a6f6e70f 100644
--- a/gajim/gtk/const.py
+++ b/gajim/gtk/const.py
@@ -223,6 +223,8 @@ MAIN_WIN_ACTIONS = [
('copy-message', 's', True),
('retract-message', 'a{sv}', False),
('quote', 's', False),
+ ('reply', 'as', False),
+ ('jump-to-message', 'au', True),
('mention', 's', False),
('send-file-httpupload', 'as', False),
('send-file-jingle', 'as', False),
diff --git a/gajim/gtk/control.py b/gajim/gtk/control.py
index 91cd4ca86..8efba1e47 100644
--- a/gajim/gtk/control.py
+++ b/gajim/gtk/control.py
@@ -27,6 +27,7 @@ from nbxmpp import JID
from nbxmpp.const import StatusCode
from nbxmpp.modules.security_labels import Displaymarking
from nbxmpp.structs import MucSubject
+from nbxmpp.structs import ReplyData
from gajim.common import app
from gajim.common import events
@@ -44,6 +45,7 @@ from gajim.common.modules.contacts import GroupchatContact
from gajim.common.modules.contacts import GroupchatParticipant
from gajim.common.modules.httpupload import HTTPFileTransfer
from gajim.common.storage.archive import ConversationRow
+from gajim.common.util.text import remove_fallback_text
from gajim.gtk.builder import get_builder
from gajim.gtk.conversation.jump_to_end_button import JumpToEndButton
@@ -95,6 +97,8 @@ class ChatControl(EventHelper):
app.window.get_action('activate-message-selection').connect(
'activate', self._on_activate_message_selection)
+ app.window.get_action('jump-to-message').connect(
+ 'activate', self._on_jump_to_message)
self.widget = cast(Gtk.Box, self._ui.get_object('control_box'))
self.widget.show_all()
@@ -347,7 +351,8 @@ class ChatControl(EventHelper):
msg_log_id=event.msg_log_id,
message_id=message_id,
stanza_id=None,
- additional_data=event.additional_data)
+ additional_data=event.additional_data,
+ reply_data=event.reply_data)
def _on_message_received(self, event: events.MessageReceived) -> None:
if not self._is_event_processable(event):
@@ -373,7 +378,8 @@ class ChatControl(EventHelper):
msg_log_id=event.msg_log_id,
message_id=event.properties.id,
stanza_id=event.stanza_id,
- additional_data=event.additional_data)
+ additional_data=event.additional_data,
+ reply_data=event.properties.reply_data)
def _on_mam_message_received(self,
event: events.MamMessageReceived) -> None:
@@ -417,7 +423,8 @@ class ChatControl(EventHelper):
msg_log_id=event.msg_log_id,
message_id=event.properties.id,
stanza_id=event.stanza_id,
- additional_data=event.additional_data)
+ additional_data=event.additional_data,
+ reply_data=event.properties.reply_data)
def _on_gc_message_received(self, event: events.GcMessageReceived) -> None:
if not self._is_event_processable(event):
@@ -439,7 +446,8 @@ class ChatControl(EventHelper):
msg_log_id=event.msg_log_id,
message_id=event.properties.id,
stanza_id=event.stanza_id,
- additional_data=event.additional_data)
+ additional_data=event.additional_data,
+ reply_data=event.properties.reply_data)
def _on_message_updated(self, event: events.MessageUpdated) -> None:
if not self._is_event_processable(event):
@@ -551,6 +559,14 @@ class ChatControl(EventHelper):
def _on_cancel_selection(self, _widget: MessageSelection) -> None:
self._scrolled_view.disable_row_selection()
+ def _on_jump_to_message(self,
+ _action: Gio.SimpleAction,
+ param: GLib.Variant
+ ) -> None:
+
+ log_line_id, timestamp = param.unpack()
+ self.scroll_to_message(log_line_id, timestamp)
+
def _on_jump_to_end(self, _button: Gtk.Button) -> None:
self.reset_view()
@@ -590,12 +606,23 @@ class ChatControl(EventHelper):
msg_log_id: int | None,
message_id: str | None,
stanza_id: str | None,
- additional_data: AdditionalDataDict | None
+ additional_data: AdditionalDataDict | None,
+ reply_data: ReplyData | None
) -> None:
if additional_data is None:
additional_data = AdditionalDataDict()
+ referred_message = None
+ if reply_data is not None:
+ referred_message = app.storage.archive.get_referred_message(
+ self._contact, reply_data.id)
+ if referred_message is not None:
+ text = remove_fallback_text(
+ text,
+ reply_data.fallback_start,
+ reply_data.fallback_end)
+
if self._allow_add_message():
self._scrolled_view.add_message(
text,
@@ -606,7 +633,8 @@ class ChatControl(EventHelper):
message_id=message_id,
stanza_id=stanza_id,
log_line_id=msg_log_id,
- additional_data=additional_data)
+ additional_data=additional_data,
+ referred_message=referred_message)
if not self._scrolled_view.get_autoscroll():
if kind == 'outgoing':
@@ -666,6 +694,16 @@ class ChatControl(EventHelper):
message_text = get_retraction_text(
self.contact.account, retracted_by, reason)
+ referred_message = None
+ if msg.reply_data is not None:
+ referred_message = app.storage.archive.get_referred_message(
+ self._contact, msg.reply_data.id)
+ if referred_message is not None:
+ message_text = remove_fallback_text(
+ message_text,
+ msg.reply_data.fallback_start,
+ msg.reply_data.fallback_end)
+
self._scrolled_view.add_message(
message_text,
kind,
@@ -676,7 +714,8 @@ class ChatControl(EventHelper):
stanza_id=msg.stanza_id,
log_line_id=msg.log_line_id,
marker=msg.marker,
- error=msg.error)
+ error=msg.error,
+ referred_message=referred_message)
def _request_messages(self, before: bool) -> list[ConversationRow]:
if before:
diff --git a/gajim/gtk/conversation/rows/message.py b/gajim/gtk/conversation/rows/message.py
index 614405993..c2df4ab00 100644
--- a/gajim/gtk/conversation/rows/message.py
+++ b/gajim/gtk/conversation/rows/message.py
@@ -39,6 +39,7 @@ from gajim.common.i18n import _
from gajim.common.i18n import is_rtl_text
from gajim.common.modules.contacts import GroupchatContact
from gajim.common.modules.contacts import GroupchatParticipant
+from gajim.common.storage.archive import ReferredMessageRow
from gajim.common.types import ChatContactT
from gajim.gtk.conversation.message_widget import MessageWidget
@@ -50,6 +51,7 @@ from gajim.gtk.conversation.rows.widgets import MoreMenuButton
from gajim.gtk.conversation.rows.widgets import NicknameLabel
from gajim.gtk.menus import get_chat_row_menu
from gajim.gtk.preview import PreviewWidget
+from gajim.gtk.referred_message_widget import ReferredMessageWidget
from gajim.gtk.util import format_fingerprint
from gajim.gtk.util import GajimPopover
@@ -70,7 +72,9 @@ class MessageRow(BaseRow):
display_marking: Displaymarking | None = None,
marker: str | None = None,
error: CommonError | StanzaError | None = None,
- log_line_id: int | None = None) -> None:
+ log_line_id: int | None = None,
+ referred_message: ReferredMessageRow | None = None
+ ) -> None:
BaseRow.__init__(self, account)
self.type = 'chat'
@@ -100,6 +104,8 @@ class MessageRow(BaseRow):
# Keep original text for message correction
self._original_text: str = text
+ self._ref_message_widget = None
+
if self._is_groupchat:
our_nick = get_group_chat_nick(self._account, self._contact.jid)
from_us = name == our_nick
@@ -115,6 +121,11 @@ class MessageRow(BaseRow):
app.preview_manager.create_preview(
text, self._message_widget, from_us, muc_context)
else:
+ if referred_message is not None:
+ self._ref_message_widget = ReferredMessageWidget(
+ self._contact,
+ referred_message)
+
self._message_widget = MessageWidget(account)
self._message_widget.add_with_styling(text, nickname=name)
if self._is_groupchat:
@@ -173,7 +184,13 @@ class MessageRow(BaseRow):
self._avatar_box = AvatarBox(self._contact, name, avatar)
self._bottom_box = Gtk.Box(spacing=6)
- self._bottom_box.add(self._message_widget)
+ if self._ref_message_widget is not None:
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3)
+ box.add(self._ref_message_widget)
+ box.add(self._message_widget)
+ self._bottom_box.add(box)
+ else:
+ self._bottom_box.add(self._message_widget)
if is_rtl_text(text):
self._bottom_box.set_halign(Gtk.Align.END)
diff --git a/gajim/gtk/conversation/view.py b/gajim/gtk/conversation/view.py
index 9c804b865..3c407d6ca 100644
--- a/gajim/gtk/conversation/view.py
+++ b/gajim/gtk/conversation/view.py
@@ -44,6 +44,7 @@ from gajim.common.modules.contacts import BareContact
from gajim.common.modules.contacts import GroupchatContact
from gajim.common.modules.httpupload import HTTPFileTransfer
from gajim.common.storage.archive import ConversationRow
+from gajim.common.storage.archive import ReferredMessageRow
from gajim.common.types import ChatContactT
from gajim.gtk.conversation.rows.base import BaseRow
@@ -506,7 +507,8 @@ class ConversationView(Gtk.ScrolledWindow):
display_marking: Displaymarking | None = None,
additional_data: AdditionalDataDict | None = None,
marker: str | None = None,
- error: CommonError | StanzaError | None = None
+ error: CommonError | StanzaError | None = None,
+ referred_message: ReferredMessageRow | None = None
) -> None:
if not timestamp:
@@ -525,7 +527,8 @@ class ConversationView(Gtk.ScrolledWindow):
display_marking=display_marking,
marker=marker,
error=error,
- log_line_id=log_line_id)
+ log_line_id=log_line_id,
+ referred_message=referred_message)
if message_id is not None:
self._message_id_row_map[message_id] = message_row
diff --git a/gajim/gtk/menus.py b/gajim/gtk/menus.py
index 419d3aa1c..c7b32730a 100644
--- a/gajim/gtk/menus.py
+++ b/gajim/gtk/menus.py
@@ -705,9 +705,24 @@ def get_chat_row_menu(contact: types.ChatContactT,
show_quote = not self_contact.role.is_visitor
else:
show_quote = False
+
if show_quote:
- menu_items.append((
- p_('Message row action', 'Quote…'), 'win.quote', text))
+ if isinstance(contact, GroupchatContact) and stanza_id is None:
+ # Use XEP-0393 quotes for MUCs without XEP-0359 IDs
+ menu_items.append((
+ p_('Message row action', 'Quote…'), 'win.quote', text))
+ else:
+ if isinstance(contact, GroupchatContact):
+ reply_to_id = stanza_id
+ resource_contact = contact.get_resource(name)
+ jid = str(resource_contact.real_jid or resource_contact.jid)
+ else:
+ reply_to_id = message_id
+ jid = contact.jid.bare
+ menu_items.append((
+ p_('Message row action', 'Reply…'),
+ 'win.reply',
+ GLib.Variant('as', [reply_to_id, jid])))
show_correction = False
if message_id is not None:
diff --git a/gajim/gtk/message_actions_box.py b/gajim/gtk/message_actions_box.py
index e5876b7e8..4733e6d76 100644
--- a/gajim/gtk/message_actions_box.py
+++ b/gajim/gtk/message_actions_box.py
@@ -28,6 +28,7 @@ from gi.repository import GObject
from gi.repository import Gtk
from nbxmpp.const import Chatstate
from nbxmpp.modules.security_labels import SecurityLabel
+from nbxmpp.structs import ReplyData
from gajim.common import app
from gajim.common.client import Client
@@ -39,12 +40,14 @@ from gajim.common.modules.contacts import BareContact
from gajim.common.modules.contacts import GroupchatContact
from gajim.common.modules.contacts import GroupchatParticipant
from gajim.common.types import ChatContactT
+from gajim.common.util.text import quote_text
from gajim.gtk.builder import get_builder
from gajim.gtk.dialogs import ErrorDialog
from gajim.gtk.menus import get_encryption_menu
from gajim.gtk.menus import get_format_menu
from gajim.gtk.message_input import MessageInputTextView
+from gajim.gtk.referred_message_widget import ReferredMessageWidget
from gajim.gtk.security_label_selector import SecurityLabelSelector
from gajim.gtk.util import open_window
@@ -66,6 +69,11 @@ class MessageActionsBox(Gtk.Grid):
# For undo
self.space_pressed = False
+ # For message replies
+ self._reply_box = None
+ self._reply_data = None
+ self._reply_quoted_text = None
+
self._ui.send_message_button.set_visible(
app.settings.get('show_send_message_button'))
app.settings.bind_signal('show_send_message_button',
@@ -73,7 +81,8 @@ class MessageActionsBox(Gtk.Grid):
'set_visible')
self._security_label_selector = SecurityLabelSelector()
- self._ui.box.pack_start(self._security_label_selector, False, True, 0)
+ self._ui.action_box.pack_start(
+ self._security_label_selector, False, True, 0)
self.msg_textview = MessageInputTextView()
self.msg_textview.get_buffer().connect('changed',
@@ -96,7 +105,7 @@ class MessageActionsBox(Gtk.Grid):
self._connect_actions()
app.plugin_manager.gui_extension_point(
- 'message_actions_box', self, self._ui.box)
+ 'message_actions_box', self, self._ui.action_box)
def get_current_contact(self) -> ChatContactT:
assert self._contact is not None
@@ -114,6 +123,7 @@ class MessageActionsBox(Gtk.Grid):
'show-emoji-chooser',
'quote',
'mention',
+ 'reply',
'correct-message',
]
@@ -154,6 +164,11 @@ class MessageActionsBox(Gtk.Grid):
assert param
self.msg_textview.insert_as_quote(param.get_string())
+ elif action_name == 'reply':
+ assert param
+ reply_to_id, jid = param.get_strv()
+ self._enable_reply_mode(reply_to_id, jid)
+
elif action_name == 'mention':
assert param
self.msg_textview.mention_participant(param.get_string())
@@ -173,6 +188,8 @@ class MessageActionsBox(Gtk.Grid):
self._client.connect_signal(
'state-changed', self._on_client_state_changed)
+ self.disable_reply_mode()
+
self._contact = contact
if isinstance(self._contact, GroupchatContact):
@@ -536,6 +553,55 @@ class MessageActionsBox(Gtk.Grid):
return False
+ def _enable_reply_mode(self, reply_to_id: str, jid: str) -> None:
+ if self._reply_data is not None:
+ # If reply was called again, remove the last reply box first
+ self.disable_reply_mode()
+
+ assert self._contact is not None
+ referred_message_row = app.storage.archive.get_referred_message(
+ self._contact, reply_to_id)
+ assert referred_message_row is not None
+
+ self._reply_quoted_text = quote_text(referred_message_row.message)
+ self._reply_data = ReplyData(
+ to=jid,
+ id=reply_to_id,
+ fallback_start=0,
+ fallback_end=len(self._reply_quoted_text))
+
+ close_button = Gtk.Button.new_from_icon_name(
+ 'window-close-symbolic', Gtk.IconSize.BUTTON)
+ close_button.set_valign(Gtk.Align.CENTER)
+ close_button.set_tooltip_text(_('Cancel'))
+ close_button.connect('clicked', self.disable_reply_mode)
+
+ ref_widget = ReferredMessageWidget(self._contact, referred_message_row)
+
+ self._reply_box = Gtk.Box(spacing=14)
+ self._reply_box.add(close_button)
+ self._reply_box.add(ref_widget)
+
+ self._ui.box.pack_start(self._reply_box, True, True, 0)
+ self._reply_box.show_all()
+
+ self.msg_textview.grab_focus()
+
+ def disable_reply_mode(self, *args: Any) -> None:
+ self._reply_data = None
+ self._reply_quoted_text = None
+ if self._reply_box is not None:
+ self._reply_box.destroy()
+ self._reply_box = None
+
+ def get_reply_data(self) -> tuple[ReplyData, str] | None:
+ if self._reply_data is None or self._reply_quoted_text is None:
+ return None
+
+ message = self.msg_textview.get_text()
+ fallback_text = f'{self._reply_quoted_text}{message}'
+ return self._reply_data, fallback_text
+
def _on_paste_clipboard(self,
texview: MessageInputTextView
) -> None:
diff --git a/gajim/gtk/message_input.py b/gajim/gtk/message_input.py
index 4b682498b..be6fe3458 100644
--- a/gajim/gtk/message_input.py
+++ b/gajim/gtk/message_input.py
@@ -38,6 +38,7 @@ from gajim.common.i18n import get_default_lang
from gajim.common.styling import PlainBlock
from gajim.common.styling import process
from gajim.common.types import ChatContactT
+from gajim.common.util.text import remove_fallback_text
from gajim.gtk.chat_action_processor import ChatActionProcessor
from gajim.gtk.const import MAX_MESSAGE_LENGTH
@@ -140,9 +141,16 @@ class MessageInputTextView(Gtk.TextView, EventHelper):
if message_row is None or message_row.message is None:
return
+ text = message_row.message
+ if message_row.reply_data is not None:
+ text = remove_fallback_text(
+ message_row.message,
+ message_row.reply_data.fallback_start,
+ message_row.reply_data.fallback_end)
+
self._set_correcting(True)
self.get_style_context().add_class('gajim-msg-correcting')
- self.insert_text(message_row.message)
+ self.insert_text(text)
def try_message_correction(self, message: str) -> str | None:
assert self._contact is not None
diff --git a/gajim/gtk/referred_message_widget.py b/gajim/gtk/referred_message_widget.py
new file mode 100644
index 000000000..53a5ed21b
--- /dev/null
+++ b/gajim/gtk/referred_message_widget.py
@@ -0,0 +1,93 @@
+# 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/>.
+
+from __future__ import annotations
+
+from datetime import datetime
+
+from gi.repository import GLib
+from gi.repository import Gtk
+from gi.repository import Pango
+
+from gajim.common import app
+from gajim.common import types
+from gajim.common.const import KindConstant
+from gajim.common.helpers import from_one_line
+from gajim.common.i18n import _
+from gajim.common.storage.archive import ReferredMessageRow
+
+
+class ReferredMessageWidget(Gtk.Box):
+ def __init__(self,
+ contact: types.ChatContactT,
+ referred_message: ReferredMessageRow
+ ) -> None:
+
+ Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
+ self.set_halign(Gtk.Align.START)
+ self.get_style_context().add_class('referred-message')
+
+ self._referred_message = referred_message
+
+ if contact.is_groupchat:
+ ref_str = _('%s wrote') % referred_message.contact_name
+ else:
+ if referred_message.kind == KindConstant.CHAT_MSG_RECV:
+ ref_str = _('%s wrote') % contact.name
+ else:
+ ref_str = _('You wrote')
+
+ icon = Gtk.Image.new_from_icon_name(
+ 'mail-reply-sender-symbolic',
+ Gtk.IconSize.BUTTON)
+ icon.get_style_context().add_class('dim-label')
+
+ name_label = Gtk.Label(label=ref_str)
+ name_label.get_style_context().add_class('dim-label')
+
+ date_time = datetime.fromtimestamp(referred_message.time)
+ time_format = from_one_line(app.settings.get('date_time_format'))
+ timestamp_label = Gtk.Label(
+ label=f'({date_time.strftime(time_format)})')
+ timestamp_label.get_style_context().add_class('dim-label')
+
+ jump_to_button = Gtk.LinkButton(label=_('[view message]'))
+ jump_to_button.connect('activate-link', self._on_jump_clicked)
+
+ meta_box = Gtk.Box(spacing=6)
+ meta_box.get_style_context().add_class('small-label')
+ meta_box.add(icon)
+ meta_box.add(name_label)
+ meta_box.add(timestamp_label)
+ meta_box.add(jump_to_button)
+
+ message_text = referred_message.message.split('\n')[0]
+ message_label = Gtk.Label(label=message_text)
+ message_label.set_halign(Gtk.Align.START)
+ message_label.set_max_width_chars(52)
+ message_label.set_ellipsize(Pango.EllipsizeMode.END)
+ message_label.get_style_context().add_class('dim-label')
+
+ self.add(meta_box)
+ self.add(message_label)
+
+ self.show_all()
+
+ def _on_jump_clicked(self, _button: Gtk.LinkButton) -> bool:
+ app.window.activate_action(
+ 'jump-to-message', GLib.Variant(
+ 'au',
+ [self._referred_message.log_line_id,
+ self._referred_message.time]))
+ return True