Compare commits
21 Commits
f2de3d9584
...
72809d024e
Author | SHA1 | Date | |
---|---|---|---|
72809d024e | |||
|
59b928d4f7 | ||
|
12902d126c | ||
|
c096e239e7 | ||
|
abbe00d70f | ||
|
e3ea5f8051 | ||
|
c656956365 | ||
|
bab99443b6 | ||
|
5959e6e35a | ||
|
6a4b8d9199 | ||
|
473205007f | ||
|
e99b7584d2 | ||
|
294bd887ba | ||
|
8eb1afb91c | ||
|
160670bea3 | ||
|
022df56c9e | ||
|
7d6fba5ac7 | ||
|
4de8e87f06 | ||
|
8521182a09 | ||
|
456525aa25 | ||
|
0e6e55a920 |
|
@ -6,6 +6,7 @@ The format is based on
|
|||
and this project adheres to
|
||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
- [0.7.5 (2024-03-07)](#075-2024-03-07)
|
||||
- [0.7.4 (2024-01-04)](#074-2024-01-04)
|
||||
- [0.7.3 (2022-01-31)](#073-2022-01-31)
|
||||
- [0.7.2 (2021-07-26)](#072-2021-07-26)
|
||||
|
@ -25,6 +26,30 @@ and this project adheres to
|
|||
- [0.4.0 (2020-03-21)](#040-2020-03-21)
|
||||
|
||||
|
||||
## 0.7.5 (2024-03-07)
|
||||
|
||||
### Added
|
||||
|
||||
- Added emoji processing, for example `:eyes:`
|
||||
|
||||
- Added /react command, to add (emoji) reactions
|
||||
|
||||
- Added displaying of spoiler tags
|
||||
|
||||
- Added /spoiler and /unspoiler commands
|
||||
|
||||
### Changed
|
||||
|
||||
- Now using Mistune 3
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed profile picture flickering in account settings UI
|
||||
|
||||
- Fixed overlaying tabs glitch in many UIs
|
||||
|
||||
- Fixed some issues when running on OpenBSD
|
||||
|
||||
## 0.7.4 (2024-01-04)
|
||||
|
||||
### Added
|
||||
|
|
|
@ -25,6 +25,7 @@ but compiling on Windows and macOS should be possible with the right tools.
|
|||
- [Common issues](#common-issues)
|
||||
- [cffi version mismatch](#cffi-version-mismatch)
|
||||
- [Type XYZ unavailable](#type-xyz-unavailable)
|
||||
- [libimagequant.so.0: cannot open shared object file: No such file or directory](#libimagequantso0-cannot-open-shared-object-file-no-such-file-or-directory)
|
||||
|
||||
|
||||
## Packages
|
||||
|
@ -379,3 +380,12 @@ sudo chmod 755 /usr/lib/qt5/qml/io/thp/pyotherside/*.so
|
|||
|
||||
Note that the Qt lib path might be `/usr/lib/qt/` instead of `/usr/lib/qt5/`,
|
||||
depending on the distro.
|
||||
|
||||
#### libimagequant.so.0: cannot open shared object file: No such file or directory
|
||||
|
||||
Solution from [here](https://stackoverflow.com/questions/77499381/libimagequant-so-0-cannot-open-shared-object-file-no-such-file-or-directory) works.
|
||||
|
||||
```sh
|
||||
sudo ln /lib/libimagequant.so.0.4 /lib/libimagequant.so.0
|
||||
```
|
||||
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
<provides>
|
||||
<binary>moment</binary>
|
||||
</provides>
|
||||
<requires>
|
||||
<display_length compare="ge">360</display_length>
|
||||
</requires>
|
||||
<project_license>LGPL-3.0-or-later</project_license>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
|
@ -56,6 +59,7 @@
|
|||
</screenshots>
|
||||
<metadata_license>FSFAP</metadata_license>
|
||||
<releases>
|
||||
<release version="0.7.5" date="2024-03-07"/>
|
||||
<release version="0.7.4" date="2024-01-04"/>
|
||||
<release version="0.7.3" date="2022-01-31"/>
|
||||
<release version="0.7.2" date="2021-07-26"/>
|
||||
|
@ -74,8 +78,4 @@
|
|||
<release version="0.4.1" date="2020-03-23"/>
|
||||
<release version="0.4.0" date="2020-03-21"/>
|
||||
</releases>
|
||||
<custom>
|
||||
<value key="Purism::form_factor">workstation</value>
|
||||
<value key="Purism::form_factor">mobile</value>
|
||||
</custom>
|
||||
</component>
|
||||
|
|
|
@ -4,10 +4,10 @@ appdirs >= 1.4.4, < 2
|
|||
cairosvg >= 2.4.2, < 3
|
||||
emoji >= 2.0, < 3.0
|
||||
filetype >= 1.0.7, < 2
|
||||
html_sanitizer >= 1.9.1, < 2
|
||||
lxml >= 4.5.1, < 5
|
||||
mistune >= 2.0.0, < 3.0
|
||||
pymediainfo >= 4.2.1, < 5
|
||||
html_sanitizer >= 1.9.1, < 3
|
||||
lxml >= 4.5.1, < 6
|
||||
mistune >= 2.0.0, < 4.0
|
||||
pymediainfo >= 4.2.1, < 7
|
||||
plyer >= 1.4.3, < 2
|
||||
sortedcontainers >= 2.2.2, < 3
|
||||
watchgod >= 0.7, < 0.8
|
||||
|
|
|
@ -16,4 +16,4 @@ documentation in the following modules first:
|
|||
__app_name__ = "moment"
|
||||
__display_name__ = "Moment"
|
||||
__reverse_dns__ = "xyz.mx-moment"
|
||||
__version__ = "0.7.4"
|
||||
__version__ = "0.7.5"
|
|
@ -20,7 +20,16 @@ import aiohttp
|
|||
import nio
|
||||
import plyer
|
||||
import pyotherside
|
||||
import simpleaudio
|
||||
|
||||
has_simpleaudio = True
|
||||
try:
|
||||
import simpleaudio
|
||||
except ImportError as e:
|
||||
trace = traceback.format_exc().rstrip()
|
||||
log.error("Importing simpleaudio failed\n%s", trace)
|
||||
has_simpleaudio = False
|
||||
|
||||
|
||||
from appdirs import AppDirs
|
||||
from nio.client.async_client import client_session
|
||||
|
||||
|
@ -590,6 +599,13 @@ class Backend:
|
|||
|
||||
|
||||
async def sound_notify(self) -> None:
|
||||
|
||||
if not has_simpleaudio:
|
||||
if self.audio_working:
|
||||
log.error("Playing audio not supported as python-simpleaudio is not installed")
|
||||
self.audio_working = False
|
||||
return
|
||||
|
||||
path = self.settings.Notifications.default_sound
|
||||
path = str(Path(path).expanduser())
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ from urllib.parse import unquote
|
|||
|
||||
import html_sanitizer.sanitizer as sanitizer
|
||||
import lxml.html # nosec
|
||||
import emoji
|
||||
import mistune
|
||||
import nio
|
||||
from html_sanitizer.sanitizer import Sanitizer
|
||||
|
@ -40,7 +41,10 @@ def plugin_matrix(md):
|
|||
# Mark colour as high priority as otherwise e.g. <red>(hi) matches the
|
||||
# inline_html rule instead of the colour rule.
|
||||
md.inline.rules.insert(1, "colour")
|
||||
md.inline.register_rule("colour", colour, parse_colour)
|
||||
if mistune.__version__.startswith("2."): # v2
|
||||
md.inline.register_rule("colour", colour, parse_colour)
|
||||
else:
|
||||
md.inline.register("colour", colour, parse_colour)
|
||||
|
||||
if md.renderer.NAME == "html":
|
||||
md.renderer.register("colour", render_html_colour)
|
||||
|
@ -215,6 +219,14 @@ class HTMLProcessor:
|
|||
if not outgoing:
|
||||
self._matrix_to_links_add_classes(a_tag)
|
||||
|
||||
for node in tree.iterdescendants():
|
||||
if node.tag != "code" and node.text:
|
||||
node.text = emoji.emojize(
|
||||
node.text, language="alias", variant="emoji_type")
|
||||
if node.getparent() and node.getparent().tag != "code" and node.tail:
|
||||
node.tail = emoji.emojize(
|
||||
node.tail, language="alias", variant="emoji_type")
|
||||
|
||||
html = etree.tostring(tree, encoding="utf-8", method="html").decode()
|
||||
html = sanit.sanitize(html).rstrip("\n")
|
||||
|
||||
|
@ -223,6 +235,27 @@ class HTMLProcessor:
|
|||
|
||||
# Client-side modifications
|
||||
|
||||
# re-parsing, will sanitize again but allowing style
|
||||
tree = etree.fromstring(
|
||||
html, parser=etree.HTMLParser(encoding="utf-8"),
|
||||
)
|
||||
|
||||
for span_tag in tree.iterfind(".//span[@data-mx-spoiler]"):
|
||||
# if there are sub-elements, their styles also need to be set or
|
||||
# background-color doesn't seem to apply
|
||||
for tag in span_tag.iter():
|
||||
tag.set(
|
||||
"style",
|
||||
"color: black !important; background-color: black !important;"
|
||||
+ (tag.get("style") or "")
|
||||
)
|
||||
|
||||
html = etree.tostring(tree, encoding="utf-8", method="html").decode()
|
||||
|
||||
html = Sanitizer(self.sanitize_settings(
|
||||
inline, outgoing, mentions, extra_attributes={"style"}
|
||||
)).sanitize(html).rstrip("\n")
|
||||
|
||||
html = self.quote_regex.sub(r'\1<span class="quote">\2</span>\3', html)
|
||||
|
||||
if not inline:
|
||||
|
@ -238,6 +271,7 @@ class HTMLProcessor:
|
|||
inline: bool = False,
|
||||
outgoing: bool = False,
|
||||
display_name_mentions: Optional[Dict[str, str]] = None,
|
||||
extra_attributes: set = set(),
|
||||
) -> dict:
|
||||
"""Return an html_sanitizer configuration."""
|
||||
|
||||
|
@ -250,16 +284,21 @@ class HTMLProcessor:
|
|||
"font": {"color"},
|
||||
"a": {"href", "class", "data-mention"},
|
||||
"code": {"class"},
|
||||
"span": {"data-mx-spoiler", "data-mx-color"},
|
||||
}
|
||||
attributes = {**inlines_attributes, **{
|
||||
"ol": {"start"},
|
||||
"hr": {"width"},
|
||||
"span": {"data-mx-color"},
|
||||
"img": {
|
||||
"data-mx-emote", "src", "alt", "title", "width", "height",
|
||||
},
|
||||
}}
|
||||
|
||||
for key in inlines_attributes:
|
||||
inlines_attributes[key] |= extra_attributes
|
||||
for key in attributes:
|
||||
attributes[key] |= extra_attributes
|
||||
|
||||
username_link_regexes = [re.compile(r) for r in [
|
||||
rf"(?<!\w)(?P<body>{re.escape(name or user_id)})(?!\w)(?P<host>)"
|
||||
for user_id, name in (display_name_mentions or {}).items()
|
||||
|
@ -272,7 +311,7 @@ class HTMLProcessor:
|
|||
"separate": {"a"} if inline else {
|
||||
"a", "p", "li", "table", "tr", "th", "td", "br", "hr", "img",
|
||||
},
|
||||
"whitespace": {},
|
||||
"whitespace": set(),
|
||||
"keep_typographic_whitespace": True,
|
||||
"add_nofollow": False,
|
||||
"autolink": {
|
||||
|
|
|
@ -151,7 +151,6 @@ class MatrixClient(nio.AsyncClient):
|
|||
"m.call.*",
|
||||
"m.room.third_party_invite",
|
||||
"m.room.tombstone",
|
||||
"m.reaction",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -179,6 +178,24 @@ class MatrixClient(nio.AsyncClient):
|
|||
if host in ("127.0.0.1", "localhost", "::1"):
|
||||
proxy = None
|
||||
|
||||
# self is passed in explicitly, so that it will also be passed to
|
||||
# handlers in the user config
|
||||
self.cmd_handler_map = {
|
||||
"me ": MatrixClient.handle_cmd_emote,
|
||||
"react ": MatrixClient.handle_cmd_react,
|
||||
"spoiler ": MatrixClient.handle_cmd_spoiler,
|
||||
"unspoiler": MatrixClient.handle_cmd_unspoiler,
|
||||
}
|
||||
|
||||
try:
|
||||
self.cmd_handler_map = {
|
||||
**self.cmd_handler_map,
|
||||
**backend.settings.Commands.get_cmd_handler_map(),
|
||||
}
|
||||
except (AttributeError, KeyError):
|
||||
# make sure we don't break older configs
|
||||
pass
|
||||
|
||||
super().__init__(
|
||||
homeserver = homeserver,
|
||||
user = user,
|
||||
|
@ -238,8 +255,6 @@ class MatrixClient(nio.AsyncClient):
|
|||
# {room_id: event}
|
||||
self.power_level_events: Dict[str, nio.PowerLevelsEvent] = {}
|
||||
|
||||
self.invalid_disconnecting: bool = False
|
||||
|
||||
self.nio_callbacks = NioCallbacks(self)
|
||||
|
||||
|
||||
|
@ -609,7 +624,6 @@ class MatrixClient(nio.AsyncClient):
|
|||
|
||||
return {**self.invited_rooms, **self.rooms}
|
||||
|
||||
|
||||
async def send_text(
|
||||
self,
|
||||
room_id: str,
|
||||
|
@ -617,22 +631,46 @@ class MatrixClient(nio.AsyncClient):
|
|||
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||
reply_to_event_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Send a markdown `m.text` or `m.notice` (with `/me`) message ."""
|
||||
if text.startswith("//") or text.startswith(r"\/"):
|
||||
await self._send_text(
|
||||
room_id, text[1:], display_name_mentions, reply_to_event_id)
|
||||
elif text.startswith("/"):
|
||||
for k, v in self.cmd_handler_map.items():
|
||||
if text.startswith("/" + k):
|
||||
await v(self, room_id, text[len("/" + k):],
|
||||
display_name_mentions, reply_to_event_id)
|
||||
break
|
||||
else:
|
||||
await self.send_fake_notice(
|
||||
room_id,
|
||||
r"That command was not recognised. "
|
||||
r"To send a message starting with /, use //",
|
||||
)
|
||||
else:
|
||||
await self._send_text(
|
||||
room_id, text, display_name_mentions, reply_to_event_id)
|
||||
|
||||
|
||||
async def _send_text(
|
||||
self,
|
||||
room_id: str,
|
||||
text: str,
|
||||
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||
reply_to_event_id: Optional[str] = None,
|
||||
override_to_html: Optional[str] = None,
|
||||
override_echo_body: Optional[str] = None,
|
||||
emote: bool = False,
|
||||
) -> None:
|
||||
"""Send a markdown `m.text` or `m.emote` message ."""
|
||||
|
||||
from_md = partial(
|
||||
HTML.from_markdown, display_name_mentions=display_name_mentions,
|
||||
)
|
||||
|
||||
escape = False
|
||||
if text.startswith("//") or text.startswith(r"\/"):
|
||||
escape = True
|
||||
text = text[1:]
|
||||
|
||||
content: Dict[str, Any]
|
||||
|
||||
if text.startswith("/me ") and not escape:
|
||||
if emote:
|
||||
event_type = nio.RoomMessageEmote
|
||||
text = text[len("/me "):]
|
||||
content = {"body": text, "msgtype": "m.emote"}
|
||||
to_html = from_md(text, inline=True, outgoing=True)
|
||||
echo_body = from_md(text, inline=True)
|
||||
|
@ -642,6 +680,12 @@ class MatrixClient(nio.AsyncClient):
|
|||
to_html = from_md(text, outgoing=True)
|
||||
echo_body = from_md(text)
|
||||
|
||||
# override_echo_body will not be effective if it is a reply.
|
||||
# echo_body is only shown before the event is received back from the
|
||||
# server, so this is fine if not ideal
|
||||
to_html = override_to_html or to_html
|
||||
echo_body = override_echo_body or echo_body
|
||||
|
||||
if to_html not in (html.escape(text), f"<p>{html.escape(text)}</p>"):
|
||||
content["format"] = "org.matrix.custom.html"
|
||||
content["formatted_body"] = to_html
|
||||
|
@ -697,6 +741,171 @@ class MatrixClient(nio.AsyncClient):
|
|||
await self.pause_while_offline()
|
||||
await self._send_message(room_id, content, tx_id)
|
||||
|
||||
async def handle_cmd_emote(
|
||||
self,
|
||||
room_id: str,
|
||||
text: str,
|
||||
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||
reply_to_event_id: Optional[str] = None,
|
||||
):
|
||||
await self._send_text(
|
||||
room_id,
|
||||
text,
|
||||
display_name_mentions,
|
||||
reply_to_event_id,
|
||||
emote=True,
|
||||
)
|
||||
|
||||
|
||||
async def handle_cmd_react(
|
||||
self,
|
||||
room_id: str,
|
||||
text: str,
|
||||
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||
reply_to_event_id: Optional[str] = None,
|
||||
) -> None:
|
||||
if reply_to_event_id is None or reply_to_event_id == "":
|
||||
await self.send_fake_notice(
|
||||
room_id, "Please reply to a message to react to it")
|
||||
else:
|
||||
reaction = emoji.emojize(
|
||||
text, language="alias", variant="emoji_type")
|
||||
await self.send_reaction(room_id, reaction, reply_to_event_id)
|
||||
|
||||
|
||||
async def handle_cmd_spoiler(
|
||||
self,
|
||||
room_id: str,
|
||||
text: str,
|
||||
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||
reply_to_event_id: Optional[str] = None,
|
||||
) -> None:
|
||||
|
||||
from_md = partial(
|
||||
HTML.from_markdown, display_name_mentions=display_name_mentions,
|
||||
)
|
||||
|
||||
to_html = from_md(text, outgoing=True)
|
||||
echo_body = from_md(text)
|
||||
|
||||
if to_html.startswith("<p>") and to_html.endswith("</p>"):
|
||||
# we want to make sure the <span> is inside the <p>, otherwise the
|
||||
# black bar will be too wide
|
||||
inner_html = to_html[len('<p>'):-len('</p>')]
|
||||
to_html = f"<p><span data-mx-spoiler>{inner_html}</span></p>"
|
||||
else:
|
||||
to_html = f"<span data-mx-spoiler>{to_html}</span>"
|
||||
|
||||
if echo_body.startswith("<p>") and echo_body.endswith("</p>"):
|
||||
inner_html = echo_body[len('<p>'):-len('</p>')]
|
||||
echo_body = f"<p><span data-mx-spoiler>{inner_html}</span></p>"
|
||||
else:
|
||||
echo_body = f"<span data-mx-spoiler>{echo_body}</span>"
|
||||
|
||||
await self._send_text(
|
||||
room_id,
|
||||
text,
|
||||
display_name_mentions,
|
||||
reply_to_event_id,
|
||||
override_to_html = to_html,
|
||||
override_echo_body = echo_body,
|
||||
)
|
||||
|
||||
|
||||
async def handle_cmd_unspoiler(
|
||||
self,
|
||||
room_id: str,
|
||||
text: str,
|
||||
display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||
reply_to_event_id: Optional[str] = None,
|
||||
) -> None:
|
||||
if reply_to_event_id is None or reply_to_event_id == "":
|
||||
await self.send_fake_notice(
|
||||
room_id,
|
||||
"Please reply to a message with /unspoiler to unspoiler it",
|
||||
)
|
||||
else:
|
||||
spoiler_event: Event = \
|
||||
self.models[self.user_id, room_id, "events"][reply_to_event_id]
|
||||
|
||||
# get formatted_body, fallback to body,
|
||||
spoiler = getattr(spoiler_event.source, "formatted_body", None) \
|
||||
or getattr(spoiler_event.source, "body", "")
|
||||
|
||||
unspoiler = re.sub(
|
||||
r"<span[^>]+data-mx-spoiler[^>]*>(.*?)</?span>", r"\1",
|
||||
spoiler,
|
||||
)
|
||||
await self.send_fake_notice(room_id, unspoiler)
|
||||
|
||||
|
||||
async def send_reaction(
|
||||
self,
|
||||
room_id: str,
|
||||
key: str,
|
||||
reacts_to: str,
|
||||
) -> None:
|
||||
|
||||
# local event id in model isn't necessarily the actual event id
|
||||
reacts_to_event = self.models[
|
||||
self.user_id, room_id, "events"][reacts_to]
|
||||
|
||||
reacts_to_event_id = reacts_to_event.event_id
|
||||
|
||||
if self.user_id in reacts_to_event.reactions.get(key, {}).get('users', []):
|
||||
await self.send_fake_notice(
|
||||
room_id,
|
||||
"Can't send the same reaction more than once",
|
||||
)
|
||||
return
|
||||
elif reacts_to_event_id.startswith("echo-"):
|
||||
await self.send_fake_notice(
|
||||
room_id,
|
||||
"Can't react to that, it's not a real event",
|
||||
)
|
||||
return
|
||||
|
||||
item_uuid = uuid4()
|
||||
|
||||
content: Dict[str, Any] = {
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"event_id": reacts_to_event_id,
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
tx_id = uuid4()
|
||||
content[f"{__reverse_dns__}.transaction_id"] = str(tx_id)
|
||||
|
||||
await self.pause_while_offline()
|
||||
try:
|
||||
await self._send_message(
|
||||
room_id, content, item_uuid, message_type = "m.reaction")
|
||||
except MatrixError as err:
|
||||
if err.m_code == "M_DUPLICATE_ANNOTATION":
|
||||
# potentially possible if the new reaction is
|
||||
# sent before the existing reaction is loaded
|
||||
await self.send_fake_notice(
|
||||
room_id,
|
||||
"Can't send the same reaction more than once",
|
||||
)
|
||||
return
|
||||
if err.m_code == "M_UNKNOWN":
|
||||
await self.send_fake_notice(
|
||||
room_id,
|
||||
"Failed to send reaction. Has the event you are reacting to fully sent yet?",
|
||||
)
|
||||
else:
|
||||
raise err
|
||||
|
||||
# only update the UI after the reaction is sent, to not be misleading
|
||||
await self._register_reaction(
|
||||
self.all_rooms[room_id],
|
||||
reacts_to_event_id,
|
||||
key,
|
||||
self.user_id,
|
||||
)
|
||||
|
||||
async def toggle_pause_transfer(
|
||||
self, room_id: str, uuid: Union[str, UUID],
|
||||
|
@ -748,7 +957,6 @@ class MatrixClient(nio.AsyncClient):
|
|||
reply_to_event_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Send a `m.file`, `m.image`, `m.audio` or `m.video` message.
|
||||
|
||||
The Matrix client-server API states that media messages can't have a
|
||||
reply attached.
|
||||
Thus, if a `reply_to_event_id` is passed, we send a pseudo-reply as two
|
||||
|
@ -1022,6 +1230,7 @@ class MatrixClient(nio.AsyncClient):
|
|||
room_id: str,
|
||||
transaction_id: UUID,
|
||||
event_type: Type[nio.Event],
|
||||
fake_event: bool = False,
|
||||
**event_fields,
|
||||
) -> None:
|
||||
"""Register a local model `Event` while waiting for the server.
|
||||
|
@ -1049,13 +1258,13 @@ class MatrixClient(nio.AsyncClient):
|
|||
|
||||
event = Event(
|
||||
id = f"echo-{transaction_id}",
|
||||
event_id = "",
|
||||
event_id = f"echo-{transaction_id}" if fake_event else "",
|
||||
event_type = event_type,
|
||||
date = datetime.now(),
|
||||
sender_id = self.user_id,
|
||||
sender_name = our_info.display_name,
|
||||
sender_avatar = our_info.avatar_url,
|
||||
is_local_echo = True,
|
||||
is_local_echo = not fake_event,
|
||||
links = Event.parse_links(content),
|
||||
**event_fields,
|
||||
)
|
||||
|
@ -1067,9 +1276,18 @@ class MatrixClient(nio.AsyncClient):
|
|||
|
||||
await self.set_room_last_event(room_id, event)
|
||||
|
||||
async def send_fake_notice(self, room_id, msg):
|
||||
await self._local_echo(
|
||||
room_id,
|
||||
uuid4(),
|
||||
nio.RoomMessageNotice,
|
||||
content = msg,
|
||||
fake_event = True,
|
||||
)
|
||||
|
||||
async def _send_message(
|
||||
self, room_id: str, content: dict, transaction_id: UUID,
|
||||
message_type: str = "m.room.message",
|
||||
) -> None:
|
||||
"""Send a message event with `content` dict to a room."""
|
||||
|
||||
|
@ -1079,7 +1297,7 @@ class MatrixClient(nio.AsyncClient):
|
|||
async with self.backend.send_locks[room_id]:
|
||||
await self.room_send(
|
||||
room_id = room_id,
|
||||
message_type = "m.room.message",
|
||||
message_type = message_type,
|
||||
content = content,
|
||||
ignore_unverified_devices = True,
|
||||
)
|
||||
|
@ -2432,13 +2650,23 @@ class MatrixClient(nio.AsyncClient):
|
|||
self,
|
||||
room: nio.MatrixRoom,
|
||||
ev: nio.ReactionEvent,
|
||||
event_id: str = "",
|
||||
event_id: str = "",
|
||||
**fields,
|
||||
) -> Event:
|
||||
) -> None:
|
||||
await self._register_reaction(room, ev.reacts_to, ev.key, ev.sender)
|
||||
await self.register_nio_event(
|
||||
room, ev, event_id, type_specifier=TypeSpecifier.Reaction,
|
||||
content=ev.key, hidden=True, **fields,
|
||||
)
|
||||
|
||||
async def _register_reaction(
|
||||
self,
|
||||
room: nio.MatrixRoom,
|
||||
reacts_to: str,
|
||||
key: str,
|
||||
sender: str,
|
||||
) -> None:
|
||||
"""Register/update a reaction."""
|
||||
reacts_to = ev.reacts_to
|
||||
key = ev.key
|
||||
sender = ev.sender
|
||||
|
||||
model = self.models[self.user_id, room.room_id, "events"]
|
||||
reacts_to_event = model.get(reacts_to)
|
||||
|
@ -2451,7 +2679,8 @@ class MatrixClient(nio.AsyncClient):
|
|||
if reacts_to_event:
|
||||
reactions = reacts_to_event.reactions
|
||||
if key not in reactions:
|
||||
reactions[key] = {"hint": emoji.demojize(key), "users": []}
|
||||
reactions[key] = {
|
||||
"hint": emoji.demojize(key, language="alias"), "users": []}
|
||||
if sender not in reactions[key]["users"]:
|
||||
reactions[key]["users"].append(sender)
|
||||
reacts_to_event.set_fields(reactions=reactions)
|
||||
|
@ -2467,11 +2696,6 @@ class MatrixClient(nio.AsyncClient):
|
|||
if sender not in registry[reacts_to][key]:
|
||||
registry[reacts_to][key].append(sender)
|
||||
|
||||
await self.register_nio_event(
|
||||
room, ev, event_id, type_specifier=TypeSpecifier.Reaction,
|
||||
content=key, hidden=True, **fields,
|
||||
)
|
||||
|
||||
async def register_message_replacement(
|
||||
self,
|
||||
room: nio.MatrixRoom,
|
||||
|
@ -2689,8 +2913,8 @@ class MatrixClient(nio.AsyncClient):
|
|||
)
|
||||
|
||||
sender = item.sender_name or item.sender_id
|
||||
is_linux = platform.system() == "Linux"
|
||||
use_html = is_linux and self.backend.settings.Notifications.use_html
|
||||
is_nux = platform.system() == "Linux" or "BSD" in platform.system()
|
||||
use_html = is_nux and self.backend.settings.Notifications.use_html
|
||||
content = item.inline_content if use_html else item.plain_content
|
||||
|
||||
if isinstance(ev, nio.RoomMessageEmote) and use_html:
|
||||
|
|
|
@ -541,3 +541,36 @@ class Keys:
|
|||
|
||||
# Sign out checked sessions if any, else sign out all sessions.
|
||||
sign_out = ["Alt+S", "Delete"]
|
||||
|
||||
class Commands:
|
||||
# If you are an advanced user, here you can define new /commands
|
||||
#
|
||||
# get_cmd_handler_map should return a dictionary of the form
|
||||
# {
|
||||
# "command": command_handling_function,
|
||||
# "another_command": ...
|
||||
# }
|
||||
#
|
||||
# each command_handling_function should have the signature:
|
||||
#
|
||||
# async def command_handling_function(
|
||||
# matrix_client: moment.backend.MatrixClient,
|
||||
# room_id: str,
|
||||
# text: str, # text after the end of /command
|
||||
# display_name_mentions: Optional[Dict[str, str]] = None, # {id: name}
|
||||
# reply_to_event_id: Optional[str] = None,
|
||||
# ) -> None:
|
||||
|
||||
# Example:
|
||||
|
||||
def get_cmd_handler_map(self):
|
||||
return {
|
||||
# "rot13 ": self.handle_cmd_rot13,
|
||||
}
|
||||
|
||||
# @staticmethod
|
||||
# def handle_cmd_rot13(matrix_client, room_id, text, display_name_mentions, reply_to_event_id):
|
||||
# import codecs
|
||||
# matrix_client._send_text(
|
||||
# room_id, codecs.encode(text, 'rot_13'), display_name_mentions, reply_to_event_id
|
||||
# )
|
|
@ -367,7 +367,7 @@ int main(int argc, char *argv[]) {
|
|||
QApplication::setOrganizationName("moment");
|
||||
QApplication::setApplicationName("moment");
|
||||
QApplication::setApplicationDisplayName("Moment");
|
||||
QApplication::setApplicationVersion("0.7.4");
|
||||
QApplication::setApplicationVersion("0.7.5");
|
||||
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
|
||||
// app needs to be constructed before attempting to migrate
|
||||
|
|
Loading…
Reference in New Issue
Block a user