Add openMessagesLinksOrFilesExternally keybind

This required us to set the media downloaded local path on events
entirely from python instead of simply lazy-fetching them when needed
from QML, due to pyotherside's async nature and files that must be open
in a certain order.
This commit is contained in:
miruka 2020-07-20 22:58:02 -04:00
parent 89e6931d9d
commit 55e22ea948
8 changed files with 96 additions and 79 deletions

View File

@ -18,7 +18,7 @@ from .matrix_client import MatrixClient
from .media_cache import MediaCache
from .models import SyncId
from .models.filters import FieldSubstringFilter
from .models.items import Account
from .models.items import Account, Event
from .models.model import Model
from .models.model_store import ModelStore
from .presence import Presence
@ -86,6 +86,9 @@ class Backend:
presences: A `{user_id: Presence}` dict for storing presence info about
matrix users registered on Mirage.
mxc_events: A dict storing media `Event` model items for any account
that have the same mxc URI
"""
def __init__(self) -> None:
@ -117,6 +120,8 @@ class Backend:
self.concurrent_get_presence_limit = asyncio.BoundedSemaphore(8)
self.mxc_events: DefaultDict[str, List[Event]] = DefaultDict(list)
def __repr__(self) -> str:
return f"{type(self).__name__}(clients={self.clients!r})"

View File

@ -693,7 +693,9 @@ class MatrixClient(nio.AsyncClient):
await asyncio.sleep(0.1)
upload_item.status = UploadStatus.Caching
await Media.from_existing_file(self.backend.media_cache, url, path)
local_media = await Media.from_existing_file(
self.backend.media_cache, url, path,
)
kind = (mime or "").split("/")[0]
@ -843,6 +845,7 @@ class MatrixClient(nio.AsyncClient):
media_size = content["info"]["size"],
media_mime = content["info"]["mimetype"],
media_crypt_dict = crypt_dict,
media_local_path = await local_media.get_local(),
thumbnail_url = thumb_url,
thumbnail_crypt_dict = thumb_crypt_dict,
@ -1926,7 +1929,7 @@ class MatrixClient(nio.AsyncClient):
event_id: str = "",
override_fetch_profile: Optional[bool] = None,
**fields,
) -> None:
) -> Event:
"""Register/update a `nio.Event` as a `models.items.Event` object."""
await self.register_nio_room(room)
@ -1988,7 +1991,7 @@ class MatrixClient(nio.AsyncClient):
# Alerts
if from_us or await self.event_is_past(ev):
return
return item
mentions_us = HTML.user_id_link_in_html(item.content, self.user_id)
AlertRequested(high_importance=mentions_us)
@ -2000,3 +2003,4 @@ class MatrixClient(nio.AsyncClient):
room_item.local_highlights = True
await self.update_account_unread_counts()
return item

View File

@ -47,30 +47,6 @@ class MediaCache:
self.downloads_dir.mkdir(parents=True, exist_ok=True)
async def get_local_media(self, mxc: str, title: str) -> Optional[Path]:
"""Return `Media.get_local()`'s result for QML."""
media = Media(self, mxc, title, None)
try:
return await media.get_local()
except FileNotFoundError:
return None
async def get_local_thumbnail(
self, mxc: str, title: str, width: int, height: int,
) -> Optional[Path]:
"""Return `Thumbnail.get_local()`'s result for QML."""
th = Thumbnail(self, mxc, title, None, (round(width), round(height)))
try:
return await th.get_local()
except FileNotFoundError:
return None
async def get_media(
self,
mxc: str,
@ -166,6 +142,10 @@ class Media:
await file.write(data)
done()
if type(self) is Media:
for event in self.cache.backend.mxc_events[self.mxc]:
event.media_local_path = self.local_path
return self.local_path

View File

@ -287,6 +287,7 @@ class Event(ModelItem):
media_size: int = 0
media_mime: str = ""
media_crypt_dict: Dict[str, Any] = field(default_factory=dict)
media_local_path: Union[str, Path] = ""
thumbnail_url: str = ""
thumbnail_mime: str = ""

View File

@ -6,12 +6,14 @@ import logging as log
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from html import escape
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from urllib.parse import quote
import nio
from .html_markdown import HTML_PROCESSOR
from .media_cache import Media
from .models.items import TypeSpecifier
from .presence import Presence
from .pyotherside_events import DevicesUpdated
@ -190,7 +192,17 @@ class NioCallbacks:
thumb_info = info.get("thumbnail_info", {})
thumb_crypt_dict = info.get("thumbnail_file", {})
await self.client.register_nio_event(
try:
media_local_path: Union[Path, str] = await Media(
cache = self.client.backend.media_cache,
mxc = ev.url,
title = ev.body,
crypt_dict = media_crypt_dict,
).get_local()
except FileNotFoundError:
media_local_path = ""
item = await self.client.register_nio_event(
room,
ev,
content = "",
@ -204,6 +216,7 @@ class NioCallbacks:
media_size = info.get("size") or 0,
media_mime = info.get("mimetype") or "",
media_crypt_dict = media_crypt_dict,
media_local_path = media_local_path,
thumbnail_url =
info.get("thumbnail_url") or thumb_crypt_dict.get("url") or "",
@ -214,6 +227,8 @@ class NioCallbacks:
thumbnail_crypt_dict = thumb_crypt_dict,
)
self.client.backend.mxc_events[ev.url].append(item)
async def onRoomEncryptedMedia(
self, room: nio.MatrixRoom, ev: nio.RoomEncryptedMedia,

View File

@ -363,9 +363,10 @@ class UISettings(JSONDataFile):
"toggleSelectMessage": ["Ctrl+Space"],
"selectMessagesUntilHere": ["Ctrl+Shift+Space"],
"removeFocusedOrSelectedMessages": ["Ctrl+R", "Alt+Del"],
"replyToFocusedOrLastMessage": ["Ctrl+Q"], # Q for Quote
"replyToFocusedOrLastMessage": ["Ctrl+Q"], # Q Quote
"debugFocusedMessage": ["Ctrl+Shift+D"],
"openMessagesLinksOrFiles": ["Ctrl+O"],
"openMessagesLinksOrFilesExternally": ["Ctrl+Shift+O"],
"clearRoomMessages": ["Ctrl+L"],
"sendFile": ["Alt+S"],

View File

@ -141,26 +141,15 @@ HColumnLayout {
property var media: []
property string link: ""
property var localPath: null
property Future getLocalFuture: null
readonly property bool isEncryptedMedia:
Object.keys(JSON.parse(model.media_crypt_dict)).length > 0
onClosed: {
if (getLocalFuture) getLocalFuture.cancel()
media = []
link = ""
}
onOpened: if (media.length === 3 && media[1].startsWith("mxc://")) {
getLocalFuture = py.callCoro(
"media_cache.get_local_media",
[media[1], media[2]],
path => { localPath = path; getLocalFuture = null },
)
}
HMenuItem {
icon.name: "toggle-select-message"
text: eventDelegate.checked ? qsTr("Deselect") : qsTr("Select")
@ -184,10 +173,10 @@ HColumnLayout {
HMenuItem {
icon.name: "copy-local-path"
text: qsTr("Copy local path")
visible: Boolean(contextMenu.localPath)
visible: Boolean(model.media_local_path)
onTriggered:
Clipboard.text =
contextMenu.localPath.replace(/^file:\/\//, "")
model.media_local_path.replace(/^file:\/\//, "")
}
HMenuItem {

View File

@ -127,26 +127,10 @@ Rectangle {
HShortcut {
sequences: window.settings.keys.openMessagesLinksOrFiles
onActivated: {
let indice = []
const indice =
eventList.getFocusedOrSelectedOrLastMediaEvents(true)
if (eventList.selectedCount) {
indice = eventList.checkedIndice
} else if (eventList.currentIndex !== -1) {
indice = [eventList.currentIndex]
} else {
// Find most recent event that's a media or contains links
for (let i = 0; i < eventList.model.count && i <= 1000; i++) {
const ev = eventList.model.get(i)
const links = JSON.parse(ev.links)
if (ev.media_url || ev.thumbnail_url || links.length) {
indice = [i]
break
}
}
}
for (const i of indice.sort().reverse()) {
for (const i of Array.from(indice).sort().reverse()) {
const event = eventList.model.get(i)
if (event.media_url || event.thumbnail_url) {
@ -166,6 +150,26 @@ Rectangle {
}
}
HShortcut {
sequences: window.settings.keys.openMessagesLinksOrFilesExternally
onActivated: {
const indice =
eventList.getFocusedOrSelectedOrLastMediaEvents(true)
for (const i of Array.from(indice).sort().reverse()) {
const event = eventList.model.get(i)
if (event.media_url) {
eventList.openMediaExternally(event)
continue
}
for (const url of JSON.parse(event.links))
Qt.openUrlExternally(url)
}
}
}
HShortcut {
active: eventList.currentItem
sequences: window.settings.keys.debugFocusedMessage
@ -318,6 +322,19 @@ Rectangle {
}
}
function getFocusedOrSelectedOrLastMediaEvents(acceptLinks=false) {
if (eventList.selectedCount) return eventList.checkedIndice
if (eventList.currentIndex !== -1) return [eventList.currentIndex]
// Find most recent event that's a media or contains links
for (let i = 0; i < eventList.model.count && i <= 1000; i++) {
const ev = eventList.model.get(i)
const links = JSON.parse(ev.links)
if (ev.media_url || (acceptLinks && links.length)) return [i]
}
}
function getMediaType(event) {
if (event.event_type === "RoomAvatarEvent")
return Utils.Media.Image
@ -402,6 +419,11 @@ Rectangle {
}
function getLocalOrDownloadMedia(event, callback) {
if (event.media_local_path) {
callback(event.media_local_path)
return
}
print("Downloading " + event.media_url + " ...")
const args = [