RoomDelegate subtitle, take only 1 arg for getUser

This commit is contained in:
miruka 2019-07-04 00:24:21 -04:00
parent 5fa2892fda
commit 63645b73a5
10 changed files with 107 additions and 91 deletions

View File

@ -1,5 +1,6 @@
import asyncio import asyncio
import json import json
import random
from pathlib import Path from pathlib import Path
from typing import Dict, Optional, Tuple from typing import Dict, Optional, Tuple
@ -7,6 +8,7 @@ from atomicfile import AtomicFile
from .app import App from .app import App
from .events import users from .events import users
from .html_filter import HTML_FILTER
from .matrix_client import MatrixClient from .matrix_client import MatrixClient
SavedAccounts = Dict[str, Dict[str, str]] SavedAccounts = Dict[str, Dict[str, str]]
@ -127,3 +129,16 @@ class Backend:
with AtomicFile(self.saved_accounts_path, "w") as new: with AtomicFile(self.saved_accounts_path, "w") as new:
new.write(js) new.write(js)
# General functions
async def request_user_update_event(self, user_id: str) -> None:
client = self.clients.get(user_id,
random.choice(tuple(self.clients.values())))
await client.request_user_update_event(user_id)
@staticmethod
def inlinify(html: str) -> str:
return HTML_FILTER.filter_inline(html)

View File

@ -7,6 +7,7 @@ import mistune
from lxml.html import HtmlElement, etree # nosec from lxml.html import HtmlElement, etree # nosec
import html_sanitizer.sanitizer as sanitizer import html_sanitizer.sanitizer as sanitizer
from html_sanitizer.sanitizer import Sanitizer
class HtmlFilter: class HtmlFilter:
@ -20,7 +21,8 @@ class HtmlFilter:
def __init__(self) -> None: def __init__(self) -> None:
self._sanitizer = sanitizer.Sanitizer(self.sanitizer_settings) self._sanitizer = Sanitizer(self.sanitize_settings())
self._inline_sanitizer = Sanitizer(self.sanitize_settings(inline=True))
# The whitespace remover doesn't take <pre> into account # The whitespace remover doesn't take <pre> into account
sanitizer.normalize_overall_whitespace = lambda html: html sanitizer.normalize_overall_whitespace = lambda html: html
@ -34,6 +36,10 @@ class HtmlFilter:
return self.filter(self._markdown_to_html(text)) return self.filter(self._markdown_to_html(text))
def filter_inline(self, html: str) -> str:
return self._inline_sanitizer.sanitize(html)
def filter(self, html: str) -> str: def filter(self, html: str) -> str:
html = self._sanitizer.sanitize(html) html = self._sanitizer.sanitize(html)
tree = etree.fromstring(html, parser=etree.HTMLParser()) tree = etree.fromstring(html, parser=etree.HTMLParser())
@ -53,33 +59,39 @@ class HtmlFilter:
return str(result, "utf-8").strip("\n") return str(result, "utf-8").strip("\n")
@property def sanitize_settings(self, inline: bool = False) -> dict:
def sanitizer_settings(self) -> dict:
# https://matrix.org/docs/spec/client_server/latest.html#m-room-message-msgtypes # https://matrix.org/docs/spec/client_server/latest.html#m-room-message-msgtypes
# TODO: mx-reply, audio, video
inline_tags = {"font", "a", "sup", "sub", "b", "i", "s", "u", "code"}
tags = inline_tags | {
"h1", "h2", "h3", "h4", "h5", "h6","blockquote",
"p", "ul", "ol", "li", "hr", "br",
"table", "thead", "tbody", "tr", "th", "td",
"pre", "img",
}
inlines_attributes = {
# TODO: translate font attrs to qt html subset
"font": {"data-mx-bg-color", "data-mx-color"},
"a": {"href"},
"code": {"class"},
}
attributes = {**inlines_attributes, **{
"img": {"width", "height", "alt", "title", "src"},
"ol": {"start"},
}}
return { return {
"tags": { "tags": inline_tags if inline else tags,
# TODO: mx-reply, audio, video "attributes": inlines_attributes if inline else attributes,
"font", "h1", "h2", "h3", "h4", "h5", "h6", "empty": {} if inline else {"hr", "br", "img"},
"blockquote", "p", "a", "ul", "ol", "sup", "sub", "li", "separate": {"a"} if inline else {
"b", "i", "s", "u", "code", "hr", "br",
"table", "thead", "tbody", "tr", "th", "td",
"pre", "img",
},
"attributes": {
# TODO: translate font attrs to qt html subset
"font": {"data-mx-bg-color", "data-mx-color"},
"a": {"href"},
"img": {"width", "height", "alt", "title", "src"},
"ol": {"start"},
"code": {"class"},
},
"empty": {"hr", "br", "img"},
"separate": {
"a", "p", "li", "table", "tr", "th", "td", "br", "hr" "a", "p", "li", "table", "tr", "th", "td", "br", "hr"
}, },
"whitespace": {}, "whitespace": {},
"add_nofollow": False, "add_nofollow": False,
"autolink": { # FIXME: arg dict not working "autolink": {
"link_regexes": self.link_regexes, "link_regexes": self.link_regexes,
"avoid_hosts": [], "avoid_hosts": [],
}, },
@ -107,7 +119,8 @@ class HtmlFilter:
if el.tag != "font": if el.tag != "font":
return el return el
if not self.sanitizer_settings["attributes"]["font"] & set(el.keys()): settings = self.sanitize_settings()
if not settings["attributes"]["font"] & set(el.keys()):
el.clear() el.clear()
return el return el

View File

@ -16,7 +16,7 @@ Row {
} }
Rectangle { Rectangle {
color: isMessage(model) ? color: Utils.eventIsMessage(model) ?
HStyle.chat.message.background : HStyle.chat.event.background HStyle.chat.message.background : HStyle.chat.event.background
//width: nameLabel.implicitWidth //width: nameLabel.implicitWidth

View File

@ -1,6 +1,7 @@
import QtQuick 2.7 import QtQuick 2.7
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
Column { Column {
id: roomEventDelegate id: roomEventDelegate
@ -16,17 +17,12 @@ Column {
roomEventListView.model.get(model.index + nth) : null roomEventListView.model.get(model.index + nth) : null
} }
function isMessage(item) {
return /^RoomMessage($|[A-Z])/.test(item.eventType)
}
property var previousItem: getPreviousItem() property var previousItem: getPreviousItem()
signal reloadPreviousItem() signal reloadPreviousItem()
onReloadPreviousItem: previousItem = getPreviousItem() onReloadPreviousItem: previousItem = getPreviousItem()
property var senderInfo: null property var senderInfo: null
Component.onCompleted: Component.onCompleted: senderInfo = models.users.getUser(model.senderId)
senderInfo = models.users.getUser(chatPage.userId, model.senderId)
readonly property bool isOwn: chatPage.userId === model.senderId readonly property bool isOwn: chatPage.userId === model.senderId
@ -36,7 +32,7 @@ Column {
readonly property bool combine: Boolean( readonly property bool combine: Boolean(
model.date && model.date &&
previousItem && previousItem.eventType && previousItem.date && previousItem && previousItem.eventType && previousItem.date &&
isMessage(previousItem) == isMessage(model) && Utils.eventIsMessage(previousItem) == Utils.eventIsMessage(model) &&
! talkBreak && ! talkBreak &&
! dayBreak && ! dayBreak &&
previousItem.senderId === model.senderId && previousItem.senderId === model.senderId &&

View File

@ -6,6 +6,7 @@ function onAccountDeleted(user_id) {
models.accounts.popWhere({"userId": user_id}, 1) models.accounts.popWhere({"userId": user_id}, 1)
} }
// TODO: get updated from nio rooms
function onUserUpdated(user_id, display_name, avatar_url, status_message) { function onUserUpdated(user_id, display_name, avatar_url, status_message) {
models.users.upsert({"userId": user_id}, { models.users.upsert({"userId": user_id}, {
"userId": user_id, "userId": user_id,

View File

@ -11,24 +11,20 @@ QtObject {
} }
property HListModel users: HListModel { property HListModel users: HListModel {
function getUser(as_account_id, wanted_user_id) { function getUser(user_id) {
wanted_user_id = wanted_user_id || as_account_id var found = users.getWhere({"userId": user_id}, 1)
var found = users.getWhere({"userId": wanted_user_id}, 1)
if (found.length > 0) { return found[0] } if (found.length > 0) { return found[0] }
users.append({ users.append({
"userId": wanted_user_id, "userId": user_id,
"displayName": "", "displayName": "",
"avatarUrl": "", "avatarUrl": "",
"statusMessage": "" "statusMessage": ""
}) })
py.callClientCoro( py.callCoro("request_user_update_event", [user_id])
as_account_id, "request_user_update_event", [wanted_user_id]
)
return users.getWhere({"userId": wanted_user_id}, 1)[0] return users.getWhere({"userId": user_id}, 1)[0]
} }
} }

View File

@ -12,6 +12,10 @@ Python {
signal willLoadAccounts(bool will) signal willLoadAccounts(bool will)
property bool loadingAccounts: false property bool loadingAccounts: false
function callSync(name, args) {
return call_sync("APP.backend." + name, args)
}
function callCoro(name, args, kwargs, callback) { function callCoro(name, args, kwargs, callback) {
call("APP.call_backend_coro", [name, args, kwargs], function(uuid){ call("APP.call_backend_coro", [name, args, kwargs], function(uuid){
pendingCoroutines[uuid] = callback || function() {} pendingCoroutines[uuid] = callback || function() {}

View File

@ -36,10 +36,30 @@ MouseArea {
Layout.maximumWidth: parent.width Layout.maximumWidth: parent.width
} }
HLabel { HRichLabel {
id: subtitleLabel id: subtitleLabel
visible: Boolean(text) visible: Boolean(text)
//text: models.timelines.getWhere({"roomId": model.roomId}, 1)[0].content text: {
for (var i = 0; i < models.timelines.count; i++) {
var item = models.timelines.get(i) // TODO: standardize
if (item.roomId == model.roomId) {
var ev = item
break
}
}
if (! ev) { return "" }
if (! Utils.eventIsMessage(ev)) {
return Utils.translatedEventContent(ev)
}
return Utils.coloredNameHtml(
models.users.getUser(ev.senderId).displayName,
ev.senderId
) + ": " + py.callSync("inlinify", [ev.content])
}
textFormat: Text.StyledText textFormat: Text.StyledText
font.pixelSize: HStyle.fontSize.small font.pixelSize: HStyle.fontSize.small

View File

@ -1,29 +0,0 @@
.import "../Chat/utils.js" as ChatJS
function getLastRoomEventText(roomId, accountId) {
var eventsModel = Backend.roomEvents.get(roomId)
if (eventsModel.count < 1) { return "" }
var ev = eventsModel.get(0)
var name = Backend.users.get(ev.dict.sender).displayName.value
var undecryptable = ev.type === "OlmEvent" || ev.type === "MegolmEvent"
if (undecryptable || ev.type.startsWith("RoomMessage")) {
var color = Qt.hsla(Backend.hueFromString(name), 0.32, 0.3, 1)
return "<font color='" + color + "'>" +
name +
":</font> " +
(undecryptable ?
"<font color='darkred'>" + qsTr("Undecryptable") + "<font>" :
ev.dict.body)
} else {
return "<font color='" + (undecryptable ? "darkred" : "#444") + "'>" +
name +
" " +
ChatJS.getEventText(ev.type, ev.dict) +
"</font>"
}
}

View File

@ -20,7 +20,7 @@ function hueFrom(string) {
} }
function avatarHue(name) { function avatarHue(name) { // TODO: bad name
return Qt.hsla( return Qt.hsla(
hueFrom(name), hueFrom(name),
HStyle.avatar.background.saturation, HStyle.avatar.background.saturation,
@ -30,7 +30,7 @@ function avatarHue(name) {
} }
function nameHue(name) { function nameHue(name) { // TODO: bad name
return Qt.hsla( return Qt.hsla(
hueFrom(name), hueFrom(name),
HStyle.displayName.saturation, HStyle.displayName.saturation,
@ -40,6 +40,13 @@ function nameHue(name) {
} }
function coloredNameHtml(name, alt_id) {
return "<font color='" + nameHue(name || stripUserId(alt_id)) + "'>" +
escapeHtml(name || alt_id) +
"</font>"
}
function escapeHtml(string) { function escapeHtml(string) {
// Replace special HTML characters by encoded alternatives // Replace special HTML characters by encoded alternatives
return string.replace("&", "&amp;") return string.replace("&", "&amp;")
@ -50,38 +57,31 @@ function escapeHtml(string) {
} }
function eventIsMessage(ev) {
return /^RoomMessage($|[A-Z])/.test(ev.eventType)
}
function translatedEventContent(ev) { function translatedEventContent(ev) {
// ev: models.timelines item // ev: models.timelines item
if (ev.translatable == false) { return ev.content } if (ev.translatable == false) { return ev.content }
// %S → sender display name // %S → sender display name
var name = models.users.getUser(ev.senderId).displayName var name = models.users.getUser(ev.senderId).displayName
var text = ev.content.replace( var text = ev.content.replace("%S", coloredNameHtml(name, ev.senderId))
"%S",
"<font color='" + nameHue(name || stripUserId(ev.senderId)) + "'>" +
escapeHtml(name || ev.senderId) +
"</font>"
)
// %T → target (event state_key) display name // %T → target (event state_key) display name
if (ev.targetUserId) { if (ev.targetUserId) {
var target_name = models.users.getUser(ev.targetUserId).displayName var tname = models.users.getUser(ev.targetUserId).displayName
text = text.replace( text = text.replace("%T", coloredNameHtml(tname, ev.targetUserId))
"%T",
"<font color='" +
nameHue(target_name || stripUserId(ev.targetUserId)) +
"'>" +
escapeHtml(target_name || ev.targetUserId) +
"</font>"
)
} }
text = qsTr(text) text = qsTr(text)
if (model.translatable == true) { return text } if (ev.translatable == true) { return text }
// Else, model.translatable should be an array of args // Else, model.translatable should be an array of args
for (var i = 0; model.translatable.length; i++) { for (var i = 0; ev.translatable.length; i++) {
text = text.arg(model.translatable[i]) text = text.arg(ev.translatable[i])
} }
return text return text
} }