diff --git a/TODO.md b/TODO.md index c52bfeff..c814dd06 100644 --- a/TODO.md +++ b/TODO.md @@ -4,6 +4,7 @@ - Handle upload errors: non existent path, path is a dir, file too big, etc - Show real progression for mxc thumbnail loadings, uploads and downloads + - Show reason under broken thumbnail icons - Support m.file thumbnails - Generate video thumbnails - GIFs can use the video player diff --git a/src/python/backend.py b/src/python/backend.py index 231b6f50..11daa80f 100644 --- a/src/python/backend.py +++ b/src/python/backend.py @@ -1,5 +1,6 @@ import asyncio import logging as log +from pathlib import Path from typing import Any, DefaultDict, Dict, List, Optional, Tuple, Union import hsluv @@ -38,6 +39,10 @@ class Backend: self.get_profile_locks: DefaultDict[str, asyncio.Lock] = \ DefaultDict(asyncio.Lock) # {user_id: lock} + from .media_cache import MediaCache + cache_dir = Path(self.app.appdirs.user_cache_dir) + self.media_cache = MediaCache(self, cache_dir) + def __repr__(self) -> str: return f"{type(self).__name__}(clients={self.clients!r})" @@ -168,28 +173,6 @@ class Backend: return (settings, ui_state, theme) - async def get_profile(self, user_id: str) -> nio.ProfileGetResponse: - if user_id in self.profile_cache: - return self.profile_cache[user_id] - - async with self.get_profile_locks[user_id]: - while True: - try: - client = next(c for c in self.clients.values()) - break - except StopIteration: - # Retry after a bit if no client was present yet - await asyncio.sleep(0.1) - - response = await client.get_profile(user_id) - - if isinstance(response, nio.ProfileGetError): - raise MatrixError.from_nio(response) - - self.profile_cache[user_id] = response - return response - - async def get_flat_sidepane_data(self) -> List[Dict[str, Any]]: data = [] @@ -210,3 +193,54 @@ class Backend: }) return data + + + # Client functions that don't need authentification + + async def _any_client(self) -> MatrixClient: + while True: + try: + return next(c for c in self.clients.values()) + except StopIteration: + # Retry after a bit if we don't have any clients yet + await asyncio.sleep(0.1) + + + async def get_profile(self, user_id: str) -> nio.ProfileGetResponse: + if user_id in self.profile_cache: + return self.profile_cache[user_id] + + async with self.get_profile_locks[user_id]: + response = await (await self._any_client()).get_profile(user_id) + + if isinstance(response, nio.ProfileGetError): + raise MatrixError.from_nio(response) + + self.profile_cache[user_id] = response + return response + + + async def thumbnail( + self, server_name: str, media_id: str, width: int, height: int, + ) -> nio.ThumbnailResponse: + + client = await self._any_client() + response = await client.thumbnail(server_name, media_id, width, height) + + if isinstance(response, nio.ThumbnailError): + raise MatrixError.from_nio(response) + + return response + + + async def download( + self, server_name: str, media_id: str, + ) -> nio.DownloadResponse: + + client = await self._any_client() + response = await client.download(server_name, media_id) + + if isinstance(response, nio.DownloadError): + raise MatrixError.from_nio(response) + + return response diff --git a/src/python/matrix_client.py b/src/python/matrix_client.py index b13ad8ee..1d58c42d 100644 --- a/src/python/matrix_client.py +++ b/src/python/matrix_client.py @@ -80,10 +80,6 @@ class MatrixClient(nio.AsyncClient): self.skipped_events: DefaultDict[str, int] = DefaultDict(lambda: 0) - from .media_cache import MediaCache - cache_dir = Path(self.backend.app.appdirs.user_cache_dir) - self.media_cache = MediaCache(self, cache_dir) - from .nio_callbacks import NioCallbacks self.nio_callbacks = NioCallbacks(self) diff --git a/src/python/media_cache.py b/src/python/media_cache.py index 7d1a12b9..b1765d4a 100644 --- a/src/python/media_cache.py +++ b/src/python/media_cache.py @@ -12,7 +12,7 @@ from PIL import Image as PILImage import nio -from .matrix_client import MatrixClient +from .backend import Backend CryptDict = Optional[Dict[str, Any]] Size = Tuple[int, int] @@ -21,12 +21,6 @@ CONCURRENT_DOWNLOADS_LIMIT = asyncio.BoundedSemaphore(8) ACCESS_LOCKS: DefaultDict[str, asyncio.Lock] = DefaultDict(asyncio.Lock) -@dataclass -class DownloadFailed(Exception): - message: str = field() - http_code: int = field() - - @dataclass class Media: cache: "MediaCache" = field() @@ -85,14 +79,11 @@ class Media: async def _get_remote_data(self) -> bytes: parsed = urlparse(self.mxc) - resp = await self.cache.client.download( + resp = await self.cache.backend.download( server_name = parsed.netloc, media_id = parsed.path.lstrip("/"), ) - if isinstance(resp, nio.DownloadError): - raise DownloadFailed(resp.message, resp.status_code) - return await self._decrypt(resp.body) @@ -181,21 +172,18 @@ class Thumbnail(Media): parsed = urlparse(self.mxc) if self.crypt_dict: - resp = await self.cache.client.download( + resp = await self.cache.backend.download( server_name = parsed.netloc, media_id = parsed.path.lstrip("/"), ) else: - resp = await self.cache.client.thumbnail( + resp = await self.cache.backend.thumbnail( server_name = parsed.netloc, media_id = parsed.path.lstrip("/"), width = self.wanted_size[0], height = self.wanted_size[1], ) - if isinstance(resp, (nio.DownloadError, nio.ThumbnailError)): - raise DownloadFailed(resp.message, resp.status_code) - decrypted = await self._decrypt(resp.body) with io.BytesIO(decrypted) as img: @@ -207,8 +195,8 @@ class Thumbnail(Media): @dataclass class MediaCache: - client: MatrixClient = field() - base_dir: Path = field() + backend: Backend = field() + base_dir: Path = field() def __post_init__(self) -> None: diff --git a/src/qml/Base/HAvatar.qml b/src/qml/Base/HAvatar.qml index 916b9f53..6e25fd0f 100644 --- a/src/qml/Base/HAvatar.qml +++ b/src/qml/Base/HAvatar.qml @@ -15,7 +15,6 @@ Rectangle { theme.controls.avatar.background.opacity ) - property string clientUserId property string name property alias mxc: avatarImage.mxc @@ -53,7 +52,6 @@ Rectangle { sourceSize.height: parent.height fillMode: Image.PreserveAspectCrop animate: false - clientUserId: avatar.clientUserId HoverHandler { id: hoverHandler } @@ -74,7 +72,6 @@ Rectangle { contentItem: HMxcImage { id: avatarToolTipImage fillMode: Image.PreserveAspectCrop - clientUserId: avatar.clientUserId mxc: avatarImage.mxc sourceSize.width: avatarToolTip.dimension diff --git a/src/qml/Base/HImage.qml b/src/qml/Base/HImage.qml index 6bf68473..546d76c9 100644 --- a/src/qml/Base/HImage.qml +++ b/src/qml/Base/HImage.qml @@ -11,6 +11,7 @@ Image { (sourceSize.width + sourceSize.height) <= 512 + property bool broken: false property bool animate: true property bool animated: Utils.urlExtension(image.source) === "gif" property alias progressBar: progressBar @@ -78,7 +79,7 @@ Image { HIcon { anchors.centerIn: parent - visible: image.status === Image.Error + visible: broken || image.status === Image.Error svgName: "broken-image" dimension: Math.max(16, Math.min(parent.width, parent.height) * 0.2) colorize: theme.colors.negativeBackground diff --git a/src/qml/Base/HMxcImage.qml b/src/qml/Base/HMxcImage.qml index dd039fa0..1bf7f558 100644 --- a/src/qml/Base/HMxcImage.qml +++ b/src/qml/Base/HMxcImage.qml @@ -20,7 +20,6 @@ HImage { } - property string clientUserId property string mxc property string sourceOverride: "" property bool thumbnail: true @@ -53,12 +52,16 @@ HImage { let args = image.thumbnail ? [image.mxc, w, h, cryptDict] : [image.mxc, cryptDict] - py.callClientCoro( - clientUserId, "media_cache." + method, args, path => { + py.callCoro("media_cache." + method, args, path => { if (! image) return if (image.cachedPath != path) image.cachedPath = path - show = image.visible - } + + image.broken = false + image.show = image.visible + + }, () => { + image.broken = true + }, ) } } diff --git a/src/qml/Chat/Banners/Banner.qml b/src/qml/Chat/Banners/Banner.qml index e81959d5..1145aab7 100644 --- a/src/qml/Chat/Banners/Banner.qml +++ b/src/qml/Chat/Banners/Banner.qml @@ -36,7 +36,6 @@ Rectangle { HUserAvatar { id: bannerAvatar - clientUserId: chatPage.userId anchors.centerIn: parent } } diff --git a/src/qml/Chat/Composer.qml b/src/qml/Chat/Composer.qml index 6510fac2..45aeb635 100644 --- a/src/qml/Chat/Composer.qml +++ b/src/qml/Chat/Composer.qml @@ -58,7 +58,6 @@ Rectangle { HUserAvatar { id: avatar - clientUserId: chatPage.userId userId: writingUserId displayName: writingUserInfo.display_name mxc: writingUserInfo.avatar_url diff --git a/src/qml/Chat/RoomHeader.qml b/src/qml/Chat/RoomHeader.qml index 1ff03f2b..af788c99 100644 --- a/src/qml/Chat/RoomHeader.qml +++ b/src/qml/Chat/RoomHeader.qml @@ -23,7 +23,6 @@ Rectangle { HRoomAvatar { id: avatar - clientUserId: chatPage.userId displayName: chatPage.roomInfo.display_name mxc: chatPage.roomInfo.avatar_url Layout.alignment: Qt.AlignTop diff --git a/src/qml/Chat/RoomSidePane/MemberDelegate.qml b/src/qml/Chat/RoomSidePane/MemberDelegate.qml index a548deb0..d7e5b097 100644 --- a/src/qml/Chat/RoomSidePane/MemberDelegate.qml +++ b/src/qml/Chat/RoomSidePane/MemberDelegate.qml @@ -7,7 +7,6 @@ HTileDelegate { backgroundColor: theme.chat.roomSidePane.member.background image: HUserAvatar { - clientUserId: chatPage.userId userId: model.user_id displayName: model.display_name mxc: model.avatar_url diff --git a/src/qml/Chat/Timeline/EventContent.qml b/src/qml/Chat/Timeline/EventContent.qml index 6f0029dc..1748213e 100644 --- a/src/qml/Chat/Timeline/EventContent.qml +++ b/src/qml/Chat/Timeline/EventContent.qml @@ -57,7 +57,6 @@ HRowLayout { HUserAvatar { id: avatar - clientUserId: chatPage.userId userId: model.sender_id displayName: model.sender_name mxc: model.sender_avatar diff --git a/src/qml/Chat/Timeline/EventImage.qml b/src/qml/Chat/Timeline/EventImage.qml index 78183f35..bc120ead 100644 --- a/src/qml/Chat/Timeline/EventImage.qml +++ b/src/qml/Chat/Timeline/EventImage.qml @@ -10,7 +10,6 @@ HMxcImage { horizontalAlignment: Image.AlignLeft animated: loader.singleMediaInfo.media_mime === "image/gif" || Utils.urlExtension(loader.mediaUrl) === "gif" - clientUserId: chatPage.userId thumbnail: ! animated && loader.thumbnailMxc mxc: thumbnail ? (loader.thumbnailMxc || loader.mediaUrl) : diff --git a/src/qml/Pages/AccountSettings/Profile.qml b/src/qml/Pages/AccountSettings/Profile.qml index 12c8d7bd..46335339 100644 --- a/src/qml/Pages/AccountSettings/Profile.qml +++ b/src/qml/Pages/AccountSettings/Profile.qml @@ -58,7 +58,6 @@ HGridLayout { property bool changed: Boolean(sourceOverride) id: avatar - clientUserId: accountSettings.userId userId: accountSettings.userId displayName: nameField.field.text mxc: accountInfo.avatar_url diff --git a/src/qml/Pages/AddChat/CreateRoom.qml b/src/qml/Pages/AddChat/CreateRoom.qml index 231f8897..e375b576 100644 --- a/src/qml/Pages/AddChat/CreateRoom.qml +++ b/src/qml/Pages/AddChat/CreateRoom.qml @@ -55,7 +55,6 @@ HBox { HRoomAvatar { id: avatar - clientUserId: userId displayName: nameField.text Layout.alignment: Qt.AlignCenter diff --git a/src/qml/Pages/AddChat/CurrentUserAvatar.qml b/src/qml/Pages/AddChat/CurrentUserAvatar.qml index 1cac18a4..ed87f59c 100644 --- a/src/qml/Pages/AddChat/CurrentUserAvatar.qml +++ b/src/qml/Pages/AddChat/CurrentUserAvatar.qml @@ -2,8 +2,7 @@ import QtQuick 2.12 import "../../Base" HUserAvatar { - clientUserId: addChatPage.userId - userId: clientUserId + userId: addChatPage.userId displayName: addChatPage.account ? addChatPage.account.display_name : "" mxc: addChatPage.account ? addChatPage.account.avatar_url : "" } diff --git a/src/qml/SidePane/AccountDelegate.qml b/src/qml/SidePane/AccountDelegate.qml index 639ad7f8..92c94176 100644 --- a/src/qml/SidePane/AccountDelegate.qml +++ b/src/qml/SidePane/AccountDelegate.qml @@ -46,7 +46,6 @@ HTileDelegate { image: HUserAvatar { - clientUserId: model.data.user_id userId: model.data.user_id displayName: model.data.display_name mxc: model.data.avatar_url diff --git a/src/qml/SidePane/RoomDelegate.qml b/src/qml/SidePane/RoomDelegate.qml index 953c079c..e8f3017d 100644 --- a/src/qml/SidePane/RoomDelegate.qml +++ b/src/qml/SidePane/RoomDelegate.qml @@ -32,7 +32,6 @@ HTileDelegate { image: HRoomAvatar { - clientUserId: model.user_id displayName: model.data.display_name mxc: model.data.avatar_url }