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:
parent
89e6931d9d
commit
55e22ea948
@ -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})"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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 = ""
|
||||
|
@ -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,
|
||||
|
@ -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"],
|
||||
|
@ -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 {
|
||||
|
@ -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 = [
|
||||
|
Loading…
Reference in New Issue
Block a user