Implement replying to event in backend

This commit is contained in:
miruka 2020-05-20 06:17:14 -04:00
parent aa8d3cf8d3
commit fb35a6ec14
7 changed files with 68 additions and 13 deletions

View File

@ -53,6 +53,18 @@ else:
CryptDict = Dict[str, Any] CryptDict = Dict[str, Any]
REPLY_FALLBACK = (
"<mx-reply>"
"<blockquote>"
'<a href="https://matrix.to/#/{room_id}/{event_id}">In reply to</a> '
'<a href="https://matrix.to/#/{user_id}">{user_id}</a>'
"<br>"
"{content}"
"</blockquote>"
"</mx-reply>"
"{reply_content}"
)
class UploadReturn(NamedTuple): class UploadReturn(NamedTuple):
"""Details for an uploaded file.""" """Details for an uploaded file."""
@ -380,7 +392,9 @@ class MatrixClient(nio.AsyncClient):
return {**self.invited_rooms, **self.rooms} return {**self.invited_rooms, **self.rooms}
async def send_text(self, room_id: str, text: str) -> None: async def send_text(
self, room_id: str, text: str, reply_to_event_id: Optional[str] = None,
) -> None:
"""Send a markdown `m.text` or `m.notice` (with `/me`) message .""" """Send a markdown `m.text` or `m.notice` (with `/me`) message ."""
from_md = partial(HTML.from_markdown, room_id=room_id) from_md = partial(HTML.from_markdown, room_id=room_id)
@ -390,6 +404,8 @@ class MatrixClient(nio.AsyncClient):
escape = True escape = True
text = text[1:] text = text[1:]
content: Dict[str, Any]
if text.startswith("/me ") and not escape: if text.startswith("/me ") and not escape:
event_type = nio.RoomMessageEmote event_type = nio.RoomMessageEmote
text = text[len("/me "): ] text = text[len("/me "): ]
@ -406,6 +422,30 @@ class MatrixClient(nio.AsyncClient):
content["format"] = "org.matrix.custom.html" content["format"] = "org.matrix.custom.html"
content["formatted_body"] = to_html content["formatted_body"] = to_html
if reply_to_event_id:
to: Event = \
self.models[self.user_id, room_id, "events"][reply_to_event_id]
content["format"] = "org.matrix.custom.html"
content["body"] = f"> <{to.sender_id}> {to.origin_body}"
to_html = REPLY_FALLBACK.format(
room_id = room_id,
event_id = reply_to_event_id,
user_id = to.sender_id,
content =
to.origin_formatted_body or html.escape(to.origin_body),
reply_content = to_html,
)
echo_body = HTML.filter(to_html)
content["formatted_body"] = HTML.filter(to_html, outgoing=True)
content["m.relates_to"] = {
"m.in_reply_to": { "event_id": reply_to_event_id },
}
# Can't use the standard Matrix transaction IDs; they're only visible # Can't use the standard Matrix transaction IDs; they're only visible
# to the sender so our other accounts wouldn't be able to replace # to the sender so our other accounts wouldn't be able to replace
# local echoes by real messages. # local echoes by real messages.
@ -414,7 +454,13 @@ class MatrixClient(nio.AsyncClient):
mentions = HTML.mentions_in_html(echo_body) mentions = HTML.mentions_in_html(echo_body)
await self._local_echo( await self._local_echo(
room_id, tx_id, event_type, content=echo_body, mentions=mentions, room_id,
tx_id,
event_type,
content = echo_body,
mentions = mentions,
origin_body = content["body"],
origin_formatted_body = content.get("formatted_body") or "",
) )
await self._send_message(room_id, content, tx_id) await self._send_message(room_id, content, tx_id)
@ -650,7 +696,8 @@ class MatrixClient(nio.AsyncClient):
room_id, room_id,
transaction_id, transaction_id,
event_type, event_type,
inline_content = path.name, inline_content = content["body"],
origin_body = content["body"],
media_url = url, media_url = url,
media_title = path.name, media_title = path.name,
media_width = content["info"].get("w", 0), media_width = content["info"].get("w", 0),
@ -1389,6 +1436,10 @@ class MatrixClient(nio.AsyncClient):
target_name = target_name, target_name = target_name,
target_avatar = target_avatar, target_avatar = target_avatar,
links = Event.parse_links(content), links = Event.parse_links(content),
origin_body = getattr(ev, "body", "") or "",
origin_formatted_body = getattr(ev, "formatted_body", "") or "",
fetch_profile = fetch_profile =
(must_fetch_sender or must_fetch_target) (must_fetch_sender or must_fetch_target)
if override_fetch_profile is None else if override_fetch_profile is None else

View File

@ -219,11 +219,13 @@ class Event(ModelItem):
sender_avatar: str = field() sender_avatar: str = field()
fetch_profile: bool = False fetch_profile: bool = False
content: str = "" origin_body: str = ""
inline_content: str = "" origin_formatted_body: str = ""
reason: str = "" content: str = ""
links: List[str] = field(default_factory=list) inline_content: str = ""
mentions: List[Tuple[str, str]] = field(default_factory=list) reason: str = ""
links: List[str] = field(default_factory=list)
mentions: List[Tuple[str, str]] = field(default_factory=list)
type_specifier: TypeSpecifier = TypeSpecifier.Unset type_specifier: TypeSpecifier = TypeSpecifier.Unset

View File

@ -22,6 +22,7 @@ Item {
property bool ready: Boolean(userInfo && roomInfo) property bool ready: Boolean(userInfo && roomInfo)
property bool longLoading: false property bool longLoading: false
property string replyToEventId: ""
property string replyToUserId: "" property string replyToUserId: ""
property string replyToDisplayName: "" property string replyToDisplayName: ""

View File

@ -69,9 +69,11 @@ HColumnPage {
} }
ReplyBar { ReplyBar {
replyToEventId: chat.replyToEventId
replyToUserId: chat.replyToUserId replyToUserId: chat.replyToUserId
replyToDisplayName: chat.replyToDisplayName replyToDisplayName: chat.replyToDisplayName
onCancel: { onCancel: {
chat.replyToEventId = ""
chat.replyToUserId = "" chat.replyToUserId = ""
chat.replyToDisplayName = "" chat.replyToDisplayName = ""
} }

View File

@ -188,7 +188,7 @@ Rectangle {
if (textArea.text === "") { return } if (textArea.text === "") { return }
const args = [chat.roomId, toSend] const args = [chat.roomId, toSend, chat.replyToEventId]
py.callClientCoro(writingUserId, "send_text", args) py.callClientCoro(writingUserId, "send_text", args)
area.clear() area.clear()

View File

@ -9,7 +9,7 @@ InfoBar {
icon.svgName: "reply-to" icon.svgName: "reply-to"
label.textFormat: Text.StyledText label.textFormat: Text.StyledText
label.text: label.text:
replyToUserId ? replyToEventId ?
utils.coloredNameHtml(replyToDisplayName, replyToUserId) : utils.coloredNameHtml(replyToDisplayName, replyToUserId) :
"" ""
@ -17,6 +17,7 @@ InfoBar {
signal cancel() signal cancel()
property string replyToEventId: ""
property string replyToUserId: "" property string replyToUserId: ""
property string replyToDisplayName: "" property string replyToDisplayName: ""
@ -25,9 +26,6 @@ InfoBar {
backgroundColor: "transparent" backgroundColor: "transparent"
icon.name: "reply-cancel" icon.name: "reply-cancel"
icon.color: theme.colors.negativeBackground icon.color: theme.colors.negativeBackground
// iconItem.small: true
// topPadding: 0
// bottomPadding: topPadding
onClicked: cancel() onClicked: cancel()
Layout.fillHeight: true Layout.fillHeight: true

View File

@ -233,6 +233,7 @@ HColumnLayout {
text: qsTr("Reply") text: qsTr("Reply")
onTriggered: { onTriggered: {
chat.replyToEventId = model.id
chat.replyToUserId = model.sender_id chat.replyToUserId = model.sender_id
chat.replyToDisplayName = model.sender_name chat.replyToDisplayName = model.sender_name
} }