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 .media_cache import MediaCache
|
||||||
from .models import SyncId
|
from .models import SyncId
|
||||||
from .models.filters import FieldSubstringFilter
|
from .models.filters import FieldSubstringFilter
|
||||||
from .models.items import Account
|
from .models.items import Account, Event
|
||||||
from .models.model import Model
|
from .models.model import Model
|
||||||
from .models.model_store import ModelStore
|
from .models.model_store import ModelStore
|
||||||
from .presence import Presence
|
from .presence import Presence
|
||||||
@ -86,6 +86,9 @@ class Backend:
|
|||||||
|
|
||||||
presences: A `{user_id: Presence}` dict for storing presence info about
|
presences: A `{user_id: Presence}` dict for storing presence info about
|
||||||
matrix users registered on Mirage.
|
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:
|
def __init__(self) -> None:
|
||||||
@ -117,6 +120,8 @@ class Backend:
|
|||||||
|
|
||||||
self.concurrent_get_presence_limit = asyncio.BoundedSemaphore(8)
|
self.concurrent_get_presence_limit = asyncio.BoundedSemaphore(8)
|
||||||
|
|
||||||
|
self.mxc_events: DefaultDict[str, List[Event]] = DefaultDict(list)
|
||||||
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{type(self).__name__}(clients={self.clients!r})"
|
return f"{type(self).__name__}(clients={self.clients!r})"
|
||||||
|
@ -693,7 +693,9 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
upload_item.status = UploadStatus.Caching
|
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]
|
kind = (mime or "").split("/")[0]
|
||||||
|
|
||||||
@ -843,6 +845,7 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
media_size = content["info"]["size"],
|
media_size = content["info"]["size"],
|
||||||
media_mime = content["info"]["mimetype"],
|
media_mime = content["info"]["mimetype"],
|
||||||
media_crypt_dict = crypt_dict,
|
media_crypt_dict = crypt_dict,
|
||||||
|
media_local_path = await local_media.get_local(),
|
||||||
|
|
||||||
thumbnail_url = thumb_url,
|
thumbnail_url = thumb_url,
|
||||||
thumbnail_crypt_dict = thumb_crypt_dict,
|
thumbnail_crypt_dict = thumb_crypt_dict,
|
||||||
@ -1926,7 +1929,7 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
event_id: str = "",
|
event_id: str = "",
|
||||||
override_fetch_profile: Optional[bool] = None,
|
override_fetch_profile: Optional[bool] = None,
|
||||||
**fields,
|
**fields,
|
||||||
) -> None:
|
) -> Event:
|
||||||
"""Register/update a `nio.Event` as a `models.items.Event` object."""
|
"""Register/update a `nio.Event` as a `models.items.Event` object."""
|
||||||
|
|
||||||
await self.register_nio_room(room)
|
await self.register_nio_room(room)
|
||||||
@ -1988,7 +1991,7 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
# Alerts
|
# Alerts
|
||||||
|
|
||||||
if from_us or await self.event_is_past(ev):
|
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)
|
mentions_us = HTML.user_id_link_in_html(item.content, self.user_id)
|
||||||
AlertRequested(high_importance=mentions_us)
|
AlertRequested(high_importance=mentions_us)
|
||||||
@ -2000,3 +2003,4 @@ class MatrixClient(nio.AsyncClient):
|
|||||||
room_item.local_highlights = True
|
room_item.local_highlights = True
|
||||||
|
|
||||||
await self.update_account_unread_counts()
|
await self.update_account_unread_counts()
|
||||||
|
return item
|
||||||
|
@ -47,30 +47,6 @@ class MediaCache:
|
|||||||
self.downloads_dir.mkdir(parents=True, exist_ok=True)
|
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(
|
async def get_media(
|
||||||
self,
|
self,
|
||||||
mxc: str,
|
mxc: str,
|
||||||
@ -166,6 +142,10 @@ class Media:
|
|||||||
await file.write(data)
|
await file.write(data)
|
||||||
done()
|
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
|
return self.local_path
|
||||||
|
|
||||||
|
|
||||||
|
@ -279,14 +279,15 @@ class Event(ModelItem):
|
|||||||
is_local_echo: bool = False
|
is_local_echo: bool = False
|
||||||
source: Optional[nio.Event] = None
|
source: Optional[nio.Event] = None
|
||||||
|
|
||||||
media_url: str = ""
|
media_url: str = ""
|
||||||
media_title: str = ""
|
media_title: str = ""
|
||||||
media_width: int = 0
|
media_width: int = 0
|
||||||
media_height: int = 0
|
media_height: int = 0
|
||||||
media_duration: int = 0
|
media_duration: int = 0
|
||||||
media_size: int = 0
|
media_size: int = 0
|
||||||
media_mime: str = ""
|
media_mime: str = ""
|
||||||
media_crypt_dict: Dict[str, Any] = field(default_factory=dict)
|
media_crypt_dict: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
media_local_path: Union[str, Path] = ""
|
||||||
|
|
||||||
thumbnail_url: str = ""
|
thumbnail_url: str = ""
|
||||||
thumbnail_mime: str = ""
|
thumbnail_mime: str = ""
|
||||||
|
@ -6,12 +6,14 @@ import logging as log
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from html import escape
|
from html import escape
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
import nio
|
import nio
|
||||||
|
|
||||||
from .html_markdown import HTML_PROCESSOR
|
from .html_markdown import HTML_PROCESSOR
|
||||||
|
from .media_cache import Media
|
||||||
from .models.items import TypeSpecifier
|
from .models.items import TypeSpecifier
|
||||||
from .presence import Presence
|
from .presence import Presence
|
||||||
from .pyotherside_events import DevicesUpdated
|
from .pyotherside_events import DevicesUpdated
|
||||||
@ -190,7 +192,17 @@ class NioCallbacks:
|
|||||||
thumb_info = info.get("thumbnail_info", {})
|
thumb_info = info.get("thumbnail_info", {})
|
||||||
thumb_crypt_dict = info.get("thumbnail_file", {})
|
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,
|
room,
|
||||||
ev,
|
ev,
|
||||||
content = "",
|
content = "",
|
||||||
@ -204,6 +216,7 @@ class NioCallbacks:
|
|||||||
media_size = info.get("size") or 0,
|
media_size = info.get("size") or 0,
|
||||||
media_mime = info.get("mimetype") or "",
|
media_mime = info.get("mimetype") or "",
|
||||||
media_crypt_dict = media_crypt_dict,
|
media_crypt_dict = media_crypt_dict,
|
||||||
|
media_local_path = media_local_path,
|
||||||
|
|
||||||
thumbnail_url =
|
thumbnail_url =
|
||||||
info.get("thumbnail_url") or thumb_crypt_dict.get("url") or "",
|
info.get("thumbnail_url") or thumb_crypt_dict.get("url") or "",
|
||||||
@ -214,6 +227,8 @@ class NioCallbacks:
|
|||||||
thumbnail_crypt_dict = thumb_crypt_dict,
|
thumbnail_crypt_dict = thumb_crypt_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.client.backend.mxc_events[ev.url].append(item)
|
||||||
|
|
||||||
|
|
||||||
async def onRoomEncryptedMedia(
|
async def onRoomEncryptedMedia(
|
||||||
self, room: nio.MatrixRoom, ev: nio.RoomEncryptedMedia,
|
self, room: nio.MatrixRoom, ev: nio.RoomEncryptedMedia,
|
||||||
|
@ -357,16 +357,17 @@ class UISettings(JSONDataFile):
|
|||||||
"10": f"{alt_or_cmd()}+0",
|
"10": f"{alt_or_cmd()}+0",
|
||||||
},
|
},
|
||||||
|
|
||||||
"unfocusOrDeselectAllMessages": ["Ctrl+D"],
|
"unfocusOrDeselectAllMessages": ["Ctrl+D"],
|
||||||
"focusPreviousMessage": ["Ctrl+Up", "Ctrl+K"],
|
"focusPreviousMessage": ["Ctrl+Up", "Ctrl+K"],
|
||||||
"focusNextMessage": ["Ctrl+Down", "Ctrl+J"],
|
"focusNextMessage": ["Ctrl+Down", "Ctrl+J"],
|
||||||
"toggleSelectMessage": ["Ctrl+Space"],
|
"toggleSelectMessage": ["Ctrl+Space"],
|
||||||
"selectMessagesUntilHere": ["Ctrl+Shift+Space"],
|
"selectMessagesUntilHere": ["Ctrl+Shift+Space"],
|
||||||
"removeFocusedOrSelectedMessages": ["Ctrl+R", "Alt+Del"],
|
"removeFocusedOrSelectedMessages": ["Ctrl+R", "Alt+Del"],
|
||||||
"replyToFocusedOrLastMessage": ["Ctrl+Q"], # Q for Quote
|
"replyToFocusedOrLastMessage": ["Ctrl+Q"], # Q → Quote
|
||||||
"debugFocusedMessage": ["Ctrl+Shift+D"],
|
"debugFocusedMessage": ["Ctrl+Shift+D"],
|
||||||
"openMessagesLinksOrFiles": ["Ctrl+O"],
|
"openMessagesLinksOrFiles": ["Ctrl+O"],
|
||||||
"clearRoomMessages": ["Ctrl+L"],
|
"openMessagesLinksOrFilesExternally": ["Ctrl+Shift+O"],
|
||||||
|
"clearRoomMessages": ["Ctrl+L"],
|
||||||
|
|
||||||
"sendFile": ["Alt+S"],
|
"sendFile": ["Alt+S"],
|
||||||
"sendFileFromPathInClipboard": ["Alt+Shift+S"],
|
"sendFileFromPathInClipboard": ["Alt+Shift+S"],
|
||||||
|
@ -141,26 +141,15 @@ HColumnLayout {
|
|||||||
|
|
||||||
property var media: []
|
property var media: []
|
||||||
property string link: ""
|
property string link: ""
|
||||||
property var localPath: null
|
|
||||||
property Future getLocalFuture: null
|
|
||||||
|
|
||||||
readonly property bool isEncryptedMedia:
|
readonly property bool isEncryptedMedia:
|
||||||
Object.keys(JSON.parse(model.media_crypt_dict)).length > 0
|
Object.keys(JSON.parse(model.media_crypt_dict)).length > 0
|
||||||
|
|
||||||
onClosed: {
|
onClosed: {
|
||||||
if (getLocalFuture) getLocalFuture.cancel()
|
|
||||||
media = []
|
media = []
|
||||||
link = ""
|
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 {
|
HMenuItem {
|
||||||
icon.name: "toggle-select-message"
|
icon.name: "toggle-select-message"
|
||||||
text: eventDelegate.checked ? qsTr("Deselect") : qsTr("Select")
|
text: eventDelegate.checked ? qsTr("Deselect") : qsTr("Select")
|
||||||
@ -184,10 +173,10 @@ HColumnLayout {
|
|||||||
HMenuItem {
|
HMenuItem {
|
||||||
icon.name: "copy-local-path"
|
icon.name: "copy-local-path"
|
||||||
text: qsTr("Copy local path")
|
text: qsTr("Copy local path")
|
||||||
visible: Boolean(contextMenu.localPath)
|
visible: Boolean(model.media_local_path)
|
||||||
onTriggered:
|
onTriggered:
|
||||||
Clipboard.text =
|
Clipboard.text =
|
||||||
contextMenu.localPath.replace(/^file:\/\//, "")
|
model.media_local_path.replace(/^file:\/\//, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
HMenuItem {
|
HMenuItem {
|
||||||
|
@ -127,26 +127,10 @@ Rectangle {
|
|||||||
HShortcut {
|
HShortcut {
|
||||||
sequences: window.settings.keys.openMessagesLinksOrFiles
|
sequences: window.settings.keys.openMessagesLinksOrFiles
|
||||||
onActivated: {
|
onActivated: {
|
||||||
let indice = []
|
const indice =
|
||||||
|
eventList.getFocusedOrSelectedOrLastMediaEvents(true)
|
||||||
|
|
||||||
if (eventList.selectedCount) {
|
for (const i of Array.from(indice).sort().reverse()) {
|
||||||
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()) {
|
|
||||||
const event = eventList.model.get(i)
|
const event = eventList.model.get(i)
|
||||||
|
|
||||||
if (event.media_url || event.thumbnail_url) {
|
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 {
|
HShortcut {
|
||||||
active: eventList.currentItem
|
active: eventList.currentItem
|
||||||
sequences: window.settings.keys.debugFocusedMessage
|
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) {
|
function getMediaType(event) {
|
||||||
if (event.event_type === "RoomAvatarEvent")
|
if (event.event_type === "RoomAvatarEvent")
|
||||||
return Utils.Media.Image
|
return Utils.Media.Image
|
||||||
@ -402,6 +419,11 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLocalOrDownloadMedia(event, callback) {
|
function getLocalOrDownloadMedia(event, callback) {
|
||||||
|
if (event.media_local_path) {
|
||||||
|
callback(event.media_local_path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
print("Downloading " + event.media_url + " ...")
|
print("Downloading " + event.media_url + " ...")
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
|
Loading…
Reference in New Issue
Block a user