Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • gajim/gajim
  • lovetox/gajim
  • ag/gajim
  • linkmauve/gajim
  • asterix/gajim
  • andre/gajim
  • mimi89999/gajim
  • bronko/gajim
  • wurstsalat/gajim
  • baitisj/gajim
  • Dicson/gajim
  • PolynomialDivision/gajim
  • troom/gajim
  • sophie-h/gajim
  • marmistrz/gajim
  • mrDoctorWho/gajim
  • orhideous/gajim
  • jjrh/gajim
  • streaps/gajim
  • jhuffine/gajim
  • maltel/gajim
  • Dominion/gajim
  • norstbox/gajim
  • synchrone/gajim
  • mick3247652/gajim
  • Yuki/gajim
  • l-n-s/gajim
  • ehuelsmann/gajim
  • hrxi/gajim
  • SaltyBones/gajim
  • rlgh/gajim
  • genofire/gajim
  • weblate/gajim
  • PapaTutuWawa/gajim
  • eta/gajim
  • jelmer/gajim
  • Ge0rG/gajim
  • TSRh/gajim
  • tolosaeduard/gajim
  • pitchum/gajim
  • mexicarne/gajim
  • prmcgs/gajim
  • mehw/gajim
  • ecxod/gajim
  • wannestas/gajim
  • XutaxKamay/gajim
  • emil/gajim-fork
  • gs/gajim
  • jurajlutter/gajim
  • Sheldon/gajim-cme
  • dexgs/gajim
  • bodqhrohro/gajim
  • Ermine/gajim
  • mesonium/gajim
  • mjk/gajim
  • nicoco/gajim
  • Polarian/gajim
  • izaya/gajim
  • kurion/gajim
  • npmania/gajim
  • ebertus/gajim
  • intelfx/gajim
  • musipusi/gajim
  • wusspuss/gajim
  • slicht/gajim
  • toms/gajim
  • singpolyma/gajim
  • Antiz/gajim
  • hendursaga/gajim
  • cve-1312/gajim
  • smemes2/gajim
  • amlor/gajim
72 results
Show changes
Commits on Source (30)
Showing
with 643 additions and 262 deletions
......@@ -14,7 +14,7 @@ repos:
- tomli
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.353
rev: v1.1.361
hooks:
- id: pyright
pass_filenames: false
......
......@@ -692,5 +692,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.2.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
</Project>
</rdf:RDF>
......@@ -17,6 +17,7 @@ from gi.repository import Gtk
from nbxmpp.client import Client as NBXMPPClient
from nbxmpp.const import ConnectionType
from nbxmpp.const import StreamError
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import JID
from gajim.common import app
......@@ -157,6 +158,7 @@ class Client(Observable, ClientModules):
self._client.set_username(self._user)
self._client.set_resource(get_resource(self._account))
self._client.set_http_session(create_http_session())
self._client.set_supported_fallback_ns([Namespace.REPLY])
self._client.subscribe('resume-failed', self._on_resume_failed)
self._client.subscribe('resume-successful', self._on_resume_successful)
......@@ -474,12 +476,15 @@ class Client(Observable, ClientModules):
self.get_module('OMEMO').encrypt_message(message)
except Exception:
log.exception('Error')
text = message.get_text(with_fallback=False)
assert text is not None
app.ged.raise_event(
MessageNotSent(client=self._client,
jid=message.jid,
message=message.text,
error=_('Encryption error'),
time=time.time()))
MessageNotSent(
client=self._client,
jid=str(message.jid),
message=text,
error=_('Encryption error'),
time=time.time()))
return
self._send_message(message)
......@@ -498,7 +503,7 @@ class Client(Observable, ClientModules):
message.set_sent_timestamp()
message.message_id = self.send_stanza(message.stanza)
if message.text is None:
if not message.has_text():
return
self.get_module('Message').send_message(message)
......
......@@ -65,7 +65,8 @@ class AvatarSize(IntEnum):
WORKSPACE = 40
WORKSPACE_EDIT = 100
CHAT = 48
NOTIFICATION = 48
MESSAGE_REPLY = 24
NOTIFICATION = 96
CALL = 100
CALL_BIG = 200
GROUP_INFO = 100
......@@ -915,6 +916,7 @@ COMMON_FEATURES = [
Namespace.JINGLE_IBB,
Namespace.AVATAR_METADATA + '+notify',
Namespace.MESSAGE_MODERATE,
Namespace.REPLY,
Namespace.OMEMO_TEMP_DL + '+notify',
Namespace.STYLING,
]
......
......@@ -383,7 +383,7 @@ class MessageCorrected(ApplicationEvent):
self._corrected_message = corrected_message
@cached_property
def message(self) -> mod.Message | None:
def original_message(self) -> mod.Message | None:
return app.storage.archive.get_corrected_message(
self._corrected_message)
......
......@@ -201,7 +201,18 @@ class MAM(BaseModule):
if not self._is_valid_request(properties):
self._log.warning('Invalid MAM Message: unknown query id %s',
properties.mam.query_id)
self._log.debug(stanza)
self._log.warning(stanza)
raise nbxmpp.NodeProcessed
stanza_id = properties.mam.id
if stanza_id is None:
self._log.warning('Unable to determine stanza id')
self._log.warning(stanza)
raise nbxmpp.NodeProcessed
if app.storage.archive.check_if_stanza_id_exists(
self._account, properties.remote_jid, stanza_id):
self._log.info('Received duplicated message from MAM: %s', stanza_id)
raise nbxmpp.NodeProcessed
def _is_valid_request(self, properties: MessageProperties) -> bool:
......@@ -298,6 +309,8 @@ class MAM(BaseModule):
self._log.warning(result)
return
self._log.warning(result)
self._log.warning('Reset archive state: %s', own_jid)
app.storage.archive.reset_mam_archive_state(
self._account, result.jid)
_, start_date = self._get_query_params()
......@@ -355,6 +368,8 @@ class MAM(BaseModule):
contact.notify('mam-sync-error', result.get_text())
return
self._log.warning(result)
self._log.warning('Reset archive state: %s', jid)
app.storage.archive.reset_mam_archive_state(
self._account, result.jid)
_, start_date = self._get_muc_query_params(jid, threshold)
......
......@@ -134,37 +134,43 @@ class Message(BaseModule):
message_id = properties.id
if message_id is None:
self._log.warning('Received message without message id')
self._log.warning(stanza)
# TODO: Make Gajim not depend on a message_id being present
message_id = get_uuid()
self._log.warning('Generating id')
self._log.debug('Generating id for message')
stanza_id = self._get_stanza_id(properties)
if (m_type != MessageType.GROUPCHAT and
direction == ChatDirection.OUTGOING):
if app.storage.archive.check_if_duplicate(
self._account, remote_jid, message_id):
self._log.info('Duplicated message received: %s', message_id)
origin_id = properties.origin_id
if (m_type == MessageType.CHAT and
direction == ChatDirection.OUTGOING and
origin_id is not None):
if app.storage.archive.check_if_message_id_exists(
self._account, remote_jid, origin_id):
self._log.info('Duplicated message received: %s', origin_id)
return
occupant = None
if m_type == MessageType.GROUPCHAT:
if direction == ChatDirection.OUTGOING:
pk = app.storage.archive.update_pending_message(
self._account, remote_jid, properties.id, stanza_id)
if pk is not None:
app.ged.raise_event(
MessageAcknowledged(account=self._account,
jid=remote_jid,
pk=pk))
return
if (m_type == MessageType.GROUPCHAT and
direction == ChatDirection.OUTGOING and
origin_id is not None):
# Use origin-id because some group chats change the message id
# on the reflection.
pk = app.storage.archive.update_pending_message(
self._account, remote_jid, origin_id, stanza_id)
if pk is not None:
app.ged.raise_event(
MessageAcknowledged(account=self._account,
jid=remote_jid,
pk=pk))
return
occupant = self._get_occupant_info(
remote_jid, direction, timestamp, properties)
message_text = properties.body
assert properties.bodies is not None
message_text = properties.bodies.get(None)
oob_data = parse_oob(properties)
encryption_data = None
......@@ -198,6 +204,13 @@ class Message(BaseModule):
updated_at=timestamp,
)
reply = None
if properties.reply_data is not None:
reply = mod.Reply(
id=properties.reply_data.id,
to=properties.reply_data.to
)
correction_id = None
if properties.correction is not None:
correction_id = properties.correction.id
......@@ -219,6 +232,7 @@ class Message(BaseModule):
occupant_=occupant,
oob=oob_data,
security_label_=securitylabel_data,
reply=reply,
thread_id_=properties.thread,
)
......@@ -251,19 +265,18 @@ class Message(BaseModule):
timestamp = properties.mam.timestamp
return dt.datetime.fromtimestamp(timestamp, tz=dt.timezone.utc)
def _get_real_jid(self, properties: MessageProperties) -> JID | None:
def _get_real_jid(
self,
properties: MessageProperties,
contact: GroupchatParticipant,
) -> JID | None:
if properties.is_mam_message:
if properties.muc_user is None:
return None
return properties.muc_user.jid
if properties.jid.is_bare:
return None
occupant_contact = self._client.get_module('Contacts').get_contact(
properties.jid, groupchat=True)
assert isinstance(occupant_contact, GroupchatParticipant)
real_jid = occupant_contact.real_jid
real_jid = contact.real_jid
if real_jid is None:
return None
return real_jid.new_as_bare()
......@@ -279,10 +292,17 @@ class Message(BaseModule):
if not properties.type.is_groupchat:
return None
if properties.jid.is_bare:
return None
contact = self._client.get_module('Contacts').get_contact(
properties.jid, groupchat=True)
assert isinstance(contact, GroupchatParticipant)
if direction == ChatDirection.OUTGOING:
real_jid = self._client.get_own_jid().new_as_bare()
else:
real_jid = self._get_real_jid(properties)
real_jid = self._get_real_jid(properties, contact)
occupant_id = self._get_occupant_id(properties) or real_jid
if occupant_id is None:
......@@ -301,9 +321,6 @@ class Message(BaseModule):
)
def _get_occupant_id(self, properties: MessageProperties) -> str | None:
if not properties.type.is_groupchat:
return None
if properties.occupant_id is None:
return None
......@@ -344,8 +361,8 @@ class Message(BaseModule):
pk = app.storage.archive.insert_row(
error_data, ignore_on_conflict=True)
if pk == -1:
self._log.warning('Received error with already known message id',
message_id)
self._log.warning(
'Received error with already known message id: %s', message_id)
return
app.ged.raise_event(
......@@ -386,7 +403,7 @@ class Message(BaseModule):
own_jid = self._con.get_own_jid()
stanza = nbxmpp.Message(to=message.jid,
body=message.text,
body=message.get_text(),
typ=message.type_,
subject=message.subject)
......@@ -394,6 +411,13 @@ class Message(BaseModule):
stanza.setTag('replace', attrs={'id': message.correct_id},
namespace=Namespace.CORRECT)
# XEP-0461
if message.reply_data is not None:
stanza.setReply(str(message.reply_data.to),
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)
......@@ -427,7 +451,7 @@ class Message(BaseModule):
# XEP-0184
if not own_jid.bare_match(message.jid):
if message.text and not message.is_groupchat:
if message.has_text() and not message.is_groupchat:
stanza.setReceiptRequest()
# Mark Message as MUC PM
......@@ -437,12 +461,12 @@ class Message(BaseModule):
# XEP-0085
if message.chatstate is not None:
stanza.setTag(message.chatstate, namespace=Namespace.CHATSTATES)
if not message.text:
if not message.has_text():
stanza.setTag('no-store',
namespace=Namespace.MSG_HINTS)
# XEP-0333
if message.text:
if message.has_text():
stanza.setMarkable()
if message.marker:
marker, id_ = message.marker
......@@ -456,13 +480,12 @@ class Message(BaseModule):
return stanza
def send_message(self, message: OutgoingMessage) -> None:
if not message.text:
if not message.has_text():
raise ValueError('Trying to send message without text')
direction = ChatDirection.OUTGOING
remote_jid = message.jid
message_text = message.text
assert message_text is not None
timestamp = dt.datetime.fromtimestamp(
message.timestamp, tz=dt.timezone.utc)
m_type = message.message_type
......@@ -520,6 +543,13 @@ class Message(BaseModule):
updated_at=timestamp,
)
reply = None
if message.reply_data is not None:
reply = mod.Reply(
id=message.reply_data.id,
to=message.reply_data.to
)
oob_data: list[mod.OOB] = []
if message.oob_url is not None:
oob_data.append(mod.OOB(url=message.oob_url, description=None))
......@@ -532,13 +562,14 @@ class Message(BaseModule):
timestamp=timestamp,
state=state,
resource=resource,
text=message_text,
text=message.get_text(with_fallback=False),
id=message.message_id,
stanza_id=None,
user_delay_ts=None,
correction_id=message.correct_id,
encryption_=encryption_data,
oob=oob_data,
reply=reply,
security_label_=securitylabel_data,
occupant_=occupant,
)
......
......@@ -9,7 +9,6 @@ from __future__ import annotations
from typing import Any
import datetime as dt
import logging
import time
from collections import defaultdict
......@@ -47,8 +46,6 @@ from gajim.common.modules.base import BaseModule
from gajim.common.modules.bits_of_binary import store_bob_data
from gajim.common.modules.contacts import GroupchatContact
from gajim.common.modules.contacts import GroupchatParticipant
from gajim.common.storage.archive import models as mod
from gajim.common.storage.base import VALUE_MISSING
from gajim.common.structs import MUCData
from gajim.common.structs import MUCPresenceData
from gajim.common.util.datetime import utc_now
......@@ -562,8 +559,6 @@ class MUC(BaseModule):
occupant = self._get_contact(properties.jid, groupchat=True)
room = self._get_contact(properties.jid.bare)
self._store_occupant_info(room, properties)
timestamp = utc_now()
if properties.is_muc_destroyed:
......@@ -677,29 +672,6 @@ class MUC(BaseModule):
self._process_occupant_presence_change(properties, presence, occupant)
def _store_occupant_info(
self,
room_contact: GroupchatContact,
properties: PresenceProperties
) -> None:
assert properties.muc_user is not None
real_jid = properties.muc_user.jid
occupant_id = properties.occupant_id or real_jid
timestamp = dt.datetime.fromtimestamp(
properties.timestamp, dt.timezone.utc)
occupant_data = mod.Occupant(
account_=self._account,
remote_jid_=room_contact.jid,
id=str(occupant_id),
real_remote_jid_=real_jid or VALUE_MISSING,
nickname=properties.jid.resource or VALUE_MISSING,
updated_at=timestamp,
)
app.storage.archive.upsert_row(occupant_data)
def _process_occupant_presence_change(
self,
properties: PresenceProperties,
......
......@@ -82,6 +82,8 @@ ALLOWED_TAGS = [
('no-permanent-store', Namespace.HINTS),
('replace', Namespace.CORRECT),
('thread', None),
('reply', Namespace.REPLY),
('fallback', Namespace.FALLBACK),
('origin-id', Namespace.SID),
]
......@@ -264,14 +266,16 @@ class OMEMO(BaseModule):
return room_jid in self._omemo_groupchats
def encrypt_message(self, event: OutgoingMessage) -> bool:
if not event.text:
if not event.has_text():
return False
client = app.get_client(self._account)
contact = client.get_module('Contacts').get_contact(event.jid)
text = event.get_text()
assert text is not None
omemo_message = self.backend.encrypt(str(event.jid),
event.text,
text,
groupchat=contact.is_groupchat)
if omemo_message is None:
raise Exception('Encryption error')
......@@ -280,7 +284,7 @@ class OMEMO(BaseModule):
node_whitelist=ALLOWED_TAGS)
if event.is_groupchat:
self._muc_temp_store[omemo_message.payload] = event.text
self._muc_temp_store[omemo_message.payload] = text
event.additional_data['encrypted'] = {
'name': 'OMEMO',
......
......@@ -783,6 +783,14 @@ class Settings:
active.append(account)
return active
def get_account_from_jid(self, jid: JID) -> str:
for account in self._account_settings:
name = self.get_account_setting(account, 'name')
hostname = self.get_account_setting(account, 'hostname')
if jid.localpart == name and jid.domain == hostname:
return account
raise ValueError(f'No account found for: {jid}')
@overload
def get_account_setting(self,
account: str,
......
......@@ -108,8 +108,14 @@ class Migration:
self._accounts: dict[str, str] = {}
self._pre_v7(user_version)
self._v8()
if user_version < 7:
self._pre_v7(user_version)
if user_version < 8:
self._v8()
if user_version < 9:
self._v9()
if user_version < 10:
self._v10()
app.ged.raise_event(DBMigrationFinished())
......@@ -214,6 +220,52 @@ class Migration:
conn.execute(sa.text('DROP TABLE IF EXISTS unread_messages'))
conn.execute(sa.text('PRAGMA user_version=8'))
def _v9(self) -> None:
statements = [
'CREATE INDEX idx_stanza_id ON message(stanza_id, fk_remote_pk, fk_account_pk);',
'PRAGMA user_version=9',
]
self._execute_multiple(statements)
def _v10(self) -> None:
self._execute_multiple(['PRAGMA foreign_keys=OFF'])
with self._engine.begin() as conn:
stmt = sa.select(mod.Message.fk_occupant_pk).union(
sa.select(mod.Moderation.fk_occupant_pk),
sa.select(mod.DisplayedMarker.fk_occupant_pk),
)
occupant_pks = set(conn.scalars(stmt))
occupant_pks.discard(None)
conn.execute(sa.delete(mod.Occupant).where(mod.Occupant.pk.not_in(occupant_pks)))
conn.commit()
with self._engine.begin() as conn:
stmt = sa.select(mod.Occupant.fk_remote_pk).union(
sa.select(mod.Occupant.fk_real_remote_pk),
sa.select(mod.SecurityLabel.fk_remote_pk),
sa.select(mod.MessageError.fk_remote_pk),
sa.select(mod.Moderation.fk_remote_pk),
sa.select(mod.DisplayedMarker.fk_remote_pk),
sa.select(mod.Receipt.fk_remote_pk),
sa.select(mod.MAMArchiveState.fk_remote_pk),
sa.select(mod.Message.fk_remote_pk),
)
occupant_pks = set(conn.scalars(stmt))
occupant_pks.discard(None)
conn.execute(sa.delete(mod.Remote).where(mod.Remote.pk.not_in(occupant_pks)))
self._execute_multiple([
'PRAGMA foreign_keys=ON',
'PRAGMA user_version=10'
])
def _process_archive_row(
self,
conn: sa.Connection,
......
......@@ -24,6 +24,7 @@ from sqlalchemy.orm import relationship
from sqlalchemy.sql import expression as expr
from sqlalchemy.types import TypeEngine
from gajim.common import app
from gajim.common.storage.base import EpochTimestampType
from gajim.common.storage.base import JIDType
from gajim.common.storage.base import JSONType
......@@ -708,7 +709,24 @@ class Message(MappedAsDataclass, Base, UtilMixin, kw_only=True):
'fk_remote_pk',
'fk_account_pk',
),
Index(
'idx_stanza_id',
'stanza_id',
'fk_remote_pk',
'fk_account_pk',
),
)
def get_last_correction(self) -> Message:
return self.corrections[-1]
def get_referenced_message(self) -> Message | None:
if self.reply is None:
return None
account = app.settings.get_account_from_jid(self.account.jid)
return app.storage.archive.get_referenced_message(
account,
self.remote.jid,
self.type,
self.reply.id
)
......@@ -37,6 +37,7 @@ from gajim.common.helpers import get_random_string
from gajim.common.storage.archive import migration
from gajim.common.storage.archive.const import ChatDirection
from gajim.common.storage.archive.const import MessageState
from gajim.common.storage.archive.const import MessageType
from gajim.common.storage.archive.models import Account
from gajim.common.storage.archive.models import Base
from gajim.common.storage.archive.models import MAMArchiveState
......@@ -51,7 +52,7 @@ from gajim.common.storage.base import VALUE_MISSING
from gajim.common.storage.base import with_session
from gajim.common.util.datetime import FIRST_UTC_DATETIME
CURRENT_USER_VERSION = 8
CURRENT_USER_VERSION = 10
log = logging.getLogger('gajim.c.storage.archive')
......@@ -306,6 +307,58 @@ class MessageArchiveStorage(AlchemyStorage):
stmt = stmt.options(*options)
return session.scalar(stmt)
@with_session
@timeit
def get_message_with_id(
self,
session: Session,
account: str,
jid: JID,
message_id: str
) -> Message | None:
fk_account_pk = self._get_account_pk(session, account)
fk_remote_pk = self._get_jid_pk(session, jid)
stmt = select(Message).where(
Message.id == message_id,
Message.fk_remote_pk == fk_remote_pk,
Message.fk_account_pk == fk_account_pk,
)
result = session.scalars(stmt).all()
if len(result) == 1:
return result[0]
self._log.warning('Found more than one message with message id %s', message_id)
return None
@with_session
@timeit
def get_message_with_stanza_id(
self,
session: Session,
account: str,
jid: JID,
stanza_id: str
) -> Message | None:
fk_account_pk = self._get_account_pk(session, account)
fk_remote_pk = self._get_jid_pk(session, jid)
stmt = select(Message).where(
Message.stanza_id == stanza_id,
Message.fk_remote_pk == fk_remote_pk,
Message.fk_account_pk == fk_account_pk,
)
result = session.scalars(stmt).all()
if len(result) == 1:
return result[0]
self._log.warning('Found more than one message with stanza id %s', stanza_id)
return None
@with_session
@timeit
def delete_message(self, session: Session, pk: int) -> None:
......@@ -331,21 +384,37 @@ class MessageArchiveStorage(AlchemyStorage):
@with_session
@timeit
def check_if_duplicate(
def check_if_message_id_exists(
self, session: Session, account: str, jid: JID, message_id: str
) -> bool:
fk_account_pk = self._get_account_pk(session, account)
fk_remote_pk = self._get_jid_pk(session, jid)
stmt = select(Message.id).where(
exists_criteria = select(Message.id).where(
Message.id == message_id,
Message.fk_remote_pk == fk_remote_pk,
Message.fk_account_pk == fk_account_pk,
)
).exists()
self._explain(session, stmt)
res = session.scalars(stmt).first()
return res is not None
res = session.scalar(select(1).where(exists_criteria))
return bool(res)
@with_session
@timeit
def check_if_stanza_id_exists(
self, session: Session, account: str, jid: JID, stanza_id: str
) -> bool:
fk_account_pk = self._get_account_pk(session, account)
fk_remote_pk = self._get_jid_pk(session, jid)
exists_criteria = select(Message.id).where(
Message.stanza_id == stanza_id,
Message.fk_remote_pk == fk_remote_pk,
Message.fk_account_pk == fk_account_pk,
).exists()
res = session.scalar(select(1).where(exists_criteria))
return bool(res)
@with_session
@timeit
......@@ -452,9 +521,7 @@ class MessageArchiveStorage(AlchemyStorage):
Load the last correctable message of a conversation by message_id.
Conditions: max 5 min old
'''
# TODO this could match multiple rows, better is to search with the pk
# TODO there is no index for message_id
fk_account_pk = self._get_account_pk(session, account)
fk_remote_pk = self._get_jid_pk(session, jid)
......@@ -500,6 +567,21 @@ class MessageArchiveStorage(AlchemyStorage):
self._explain(session, stmt)
return session.scalar(stmt)
def get_referenced_message(
self,
account: str,
jid: JID,
message_type: MessageType | int,
reply_id: str
) -> Message | None:
if message_type == MessageType.GROUPCHAT:
return app.storage.archive.get_message_with_stanza_id(
account, jid, reply_id)
return app.storage.archive.get_message_with_id(
account, jid, reply_id)
@with_session
@timeit
def search_archive(
......@@ -785,7 +867,6 @@ class MessageArchiveStorage(AlchemyStorage):
Message.fk_remote_pk == fk_remote_pk,
Message.fk_account_pk == fk_account_pk,
Message.direction == ChatDirection.OUTGOING,
Message.state == MessageState.PENDING,
)
.values(state=MessageState.ACKNOWLEDGED, stanza_id=stanza_id)
.returning(Message.pk)
......
......@@ -93,6 +93,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,
nodes: Any | None = None,
play_sound: bool = True
......@@ -106,7 +107,7 @@ class OutgoingMessage:
self.account = account
self.contact = contact
self.text = text
self._text = text
self.type_ = type_
if type_ == 'chat':
......@@ -127,6 +128,8 @@ class OutgoingMessage:
self.control = control
self.attention = attention
self.correct_id = correct_id
self.reply_data = reply_data
self.oob_url = oob_url
self.nodes = nodes
self.play_sound = play_sound
......@@ -139,6 +142,18 @@ class OutgoingMessage:
self.stanza = None
self.delayed = None # TODO never set
def get_text(self, with_fallback: bool = True) -> str | None:
if not with_fallback:
return self._text
if self.reply_data is None:
return self._text
assert self._text is not None
return f'{self.reply_data.fallback_text}{self._text}'
def has_text(self) -> bool:
return bool(self._text)
@property
def jid(self) -> JID:
return self.contact.jid
......@@ -328,3 +343,12 @@ class VariantMixin:
raise ValueError(f'no conversion for: {value_t_name}')
vdict[field_name] = conversion_func(value)
return cls(**vdict)
@dataclass
class ReplyData:
to: JID
id: str
fallback_start: int
fallback_end: int
fallback_text: str
......@@ -40,6 +40,10 @@ def jid_to_iri(jid: str) -> str:
return 'xmpp:' + escape_iri_path(jid)
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
......
......@@ -11,9 +11,9 @@ from typing import Any
import logging
from collections.abc import Callable
from winsdk.windows.ui import Color
from winsdk.windows.ui.viewmanagement import UIColorType
from winsdk.windows.ui.viewmanagement import UISettings
from winrt.windows.ui import Color
from winrt.windows.ui.viewmanagement import UIColorType
from winrt.windows.ui.viewmanagement import UISettings
from gajim.common import app
from gajim.common.events import StyleChanged
......
......@@ -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">6</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>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clock"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-clock"
version="1.1"
id="svg1"
sodipodi:docname="feather-clock-symbolic.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32.25"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="1920"
inkscape:window-height="999"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
style="color:#000000;fill:#000000;-inkscape-stroke:none;stroke:none"
d="M 12,1 C 5.9367185,1 1,5.9367185 1,12 1,18.063282 5.9367185,23 12,23 18.063282,23 23,18.063282 23,12 23,5.9367185 18.063282,1 12,1 Z m 0,2 c 4.982402,0 9,4.0175976 9,9 0,4.982402 -4.017598,9 -9,9 C 7.0175976,21 3,16.982402 3,12 3,7.0175976 7.0175976,3 12,3 Z"
id="circle1" />
<path
style="color:#000000;fill:#000000;-inkscape-stroke:none;stroke:none"
d="m 12,5 a 1,1 0 0 0 -1,1 v 6 a 1.0001,1.0001 0 0 0 0.552734,0.894531 l 4,2 a 1,1 0 0 0 1.341797,-0.447265 1,1 0 0 0 -0.447265,-1.341797 L 13,11.382813 V 6 A 1,1 0 0 0 12,5 Z"
id="polyline1" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye-off"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-eye-off"
version="1.1"
id="svg1"
sodipodi:docname="feather-eye-off-symbolic.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32.25"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="1920"
inkscape:window-height="999"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
style="color:#000000;fill:#000000;-inkscape-stroke:none;stroke:none"
d="m 12,3 c -0.783796,-0.00169 -1.564951,0.086986 -2.328125,0.265625 A 1,1 0 0 0 8.9257813,4.46875 1,1 0 0 0 10.126953,5.2128906 C 10.739811,5.0694368 11.368626,4.9985209 11.998047,5 A 1.0001,1.0001 0 0 0 12,5 c 5.874756,0 9.369568,6.24427 9.789062,7.013672 -0.511586,0.884452 -1.05651,1.750828 -1.714843,2.533203 a 1,1 0 0 0 0.121093,1.408203 1,1 0 0 0 1.410157,-0.121094 c 0.873868,-1.03852 1.637512,-2.166291 2.277343,-3.363281 a 1.0001,1.0001 0 0 0 0.01172,-0.917969 c 0,0 -4.131995,-8.5512371 -11.892578,-8.552734 z M 6.1933594,5.0683594 A 1,1 0 0 0 5.453125,5.265625 C 3.2489512,6.9487566 1.4304401,9.0836154 0.11914062,11.527344 a 1.0001,1.0001 0 0 0 -0.0136719,0.919922 C 0.10546875,12.447266 4.238103,21 12,21 a 1.0001,1.0001 0 0 0 0.01563,0 c 2.362389,-0.03862 4.652195,-0.833311 6.53125,-2.265625 a 1,1 0 0 0 0.1875,-1.400391 1,1 0 0 0 -1.400391,-0.189453 c -1.537604,1.172043 -3.408859,1.82018 -5.341796,1.853516 C 6.123268,18.991804 2.631376,12.7593 2.2109375,11.988281 3.3600942,10.010026 4.8446693,8.2462691 6.6660156,6.8554687 A 1,1 0 0 0 6.8554687,5.453125 1,1 0 0 0 6.1933594,5.0683594 Z m 3.0058594,4.0800781 c -0.9428287,0.8785365 -1.379567,2.0063985 -1.3320313,3.0566405 0.047536,1.050242 0.5286437,1.995441 1.2304688,2.697266 0.701825,0.701825 1.6470237,1.182933 2.6972657,1.230468 1.050242,0.04754 2.178104,-0.389202 3.056641,-1.332031 a 1,1 0 0 0 -0.05078,-1.412109 1,1 0 0 0 -1.412109,0.04883 c -0.533883,0.572952 -1.020639,0.719139 -1.503906,0.697266 -0.483268,-0.02187 -0.989033,-0.26247 -1.373047,-0.646485 C 10.127704,13.104267 9.8871079,12.598502 9.8652344,12.115234 9.8433608,11.631967 9.9895478,11.145211 10.5625,10.611328 a 1,1 0 0 0 0.04883,-1.4121092 1,1 0 0 0 -1.4121092,-0.050781 z"
id="path1" />
<path
style="color:#000000;fill:#000000;-inkscape-stroke:none;stroke:none"
d="m 1,0 a 1,1 0 0 0 -0.70703125,0.29296875 1,1 0 0 0 0,1.41406245 L 22.292969,23.707031 a 1,1 0 0 0 1.414062,0 1,1 0 0 0 0,-1.414062 L 1.7070312,0.29296875 A 1,1 0 0 0 1,0 Z"
id="line1" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-eye"
version="1.1"
id="svg1"
sodipodi:docname="feather-eye-symbolic.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32.25"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="1920"
inkscape:window-height="999"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
style="color:#000000;fill:#000000;-inkscape-stroke:none;stroke:none"
d="M 12,3 C 4.238103,3 0.10546875,11.552734 0.10546875,11.552734 a 1.0001,1.0001 0 0 0 0,0.894532 C 0.10546875,12.447266 4.238103,21 12,21 c 7.761897,0 11.894531,-8.552734 11.894531,-8.552734 a 1.0001,1.0001 0 0 0 0,-0.894532 C 23.894531,11.552734 19.761897,3 12,3 Z m 0,2 c 5.863384,0 9.347664,6.209585 9.779297,7 C 21.347664,12.790415 17.863384,19 12,19 6.1366164,19 2.6523361,12.790415 2.2207031,12 2.6523361,11.209585 6.1366164,5 12,5 Z"
id="path1" />
<path
style="color:#000000;fill:#000000;-inkscape-stroke:none;stroke:none"
d="m 12,8 c -2.1972922,0 -4,1.8027078 -4,4 0,2.197292 1.8027078,4 4,4 2.197292,0 4,-1.802708 4,-4 0,-2.1972922 -1.802708,-4 -4,-4 z m 0,2 c 1.116413,0 2,0.883587 2,2 0,1.116413 -0.883587,2 -2,2 -1.116413,0 -2,-0.883587 -2,-2 0,-1.116413 0.883587,-2 2,-2 z"
id="circle1" />
</svg>