Show an uploads bar in chats when uploading files

This commit is contained in:
miruka 2019-11-05 18:31:16 -04:00
parent 91064fc625
commit 078cf61b7e
6 changed files with 189 additions and 8 deletions

View File

@ -1,4 +1,5 @@
- Media - Media
- SVG uploads
- Uploading progress bar (+local echo) - Uploading progress bar (+local echo)
- Directly create cache files for our uploads before actually uploading - Directly create cache files for our uploads before actually uploading
- Downloading - Downloading

View File

@ -9,7 +9,7 @@ import nio
from .app import App from .app import App
from .matrix_client import MatrixClient from .matrix_client import MatrixClient
from .models.items import Account, Device, Event, Member, Room from .models.items import Account, Device, Event, Member, Room, Upload
from .models.model_store import ModelStore from .models.model_store import ModelStore
ProfileResponse = Union[nio.ProfileGetResponse, nio.ProfileGetError] ProfileResponse = Union[nio.ProfileGetResponse, nio.ProfileGetError]
@ -29,6 +29,7 @@ class Backend:
(Device, str), # Devices of user_id (Device, str), # Devices of user_id
(Room, str), # Rooms for user_id (Room, str), # Rooms for user_id
(Member, str), # Members in room_id (Member, str), # Members in room_id
(Upload, str), # Uploads running in room_id
(Event, str, str), # Events for account user_id for room_id (Event, str, str), # Events for account user_id for room_id
}) })

View File

@ -25,7 +25,9 @@ import nio
from . import __about__, utils from . import __about__, utils
from .html_filter import HTML_FILTER from .html_filter import HTML_FILTER
from .models.items import Account, Event, Member, Room, TypeSpecifier from .models.items import (
Account, Event, Member, Room, TypeSpecifier, Upload, UploadStatus,
)
from .models.model_store import ModelStore from .models.model_store import ModelStore
from .pyotherside_events import AlertRequested from .pyotherside_events import AlertRequested
@ -224,9 +226,15 @@ class MatrixClient(nio.AsyncClient):
async def send_file(self, room_id: str, path: Union[Path, str]) -> None: async def send_file(self, room_id: str, path: Union[Path, str]) -> None:
path = Path(path) path = Path(path)
size = path.resolve().stat().st_size
encrypt = room_id in self.encrypted_rooms encrypt = room_id in self.encrypted_rooms
url, mime, crypt_dict = await self.upload_file(path, encrypt=encrypt) upload_item = Upload(str(path), total_size=size)
self.models[Upload, room_id][upload_item.uuid] = upload_item
url, mime, crypt_dict = await self.upload_file(
path, upload_item, encrypt=encrypt,
)
kind = (mime or "").split("/")[0] kind = (mime or "").split("/")[0]
@ -234,7 +242,7 @@ class MatrixClient(nio.AsyncClient):
"body": path.name, "body": path.name,
"info": { "info": {
"mimetype": mime, "mimetype": mime,
"size": path.resolve().stat().st_size, "size": size,
}, },
} }
@ -254,7 +262,9 @@ class MatrixClient(nio.AsyncClient):
try: try:
thumb_url, thumb_info, thumb_crypt_dict = \ thumb_url, thumb_info, thumb_crypt_dict = \
await self.upload_thumbnail(path, encrypt=encrypt) await self.upload_thumbnail(
path, upload_item, encrypt=encrypt,
)
except (UneededThumbnail, UnthumbnailableError): except (UneededThumbnail, UnthumbnailableError):
pass pass
else: else:
@ -302,6 +312,9 @@ class MatrixClient(nio.AsyncClient):
content["msgtype"] = "m.file" content["msgtype"] = "m.file"
content["filename"] = path.name content["filename"] = path.name
upload_item.status = UploadStatus.Success
del self.models[Upload, room_id]
uuid = str(uuid4()) uuid = str(uuid4())
await self._local_echo( await self._local_echo(
@ -435,7 +448,10 @@ class MatrixClient(nio.AsyncClient):
async def upload_thumbnail( async def upload_thumbnail(
self, path: Union[Path, str], encrypt: bool = False, self,
path: Union[Path, str],
item: Optional[Upload] = None,
encrypt: bool = False,
) -> Tuple[str, Dict[str, Any], CryptDict]: ) -> Tuple[str, Dict[str, Any], CryptDict]:
png_modes = ("1", "L", "P", "RGBA") png_modes = ("1", "L", "P", "RGBA")
@ -450,6 +466,9 @@ class MatrixClient(nio.AsyncClient):
if small and is_jpg_png and not jpgable_png: if small and is_jpg_png and not jpgable_png:
raise UneededThumbnail() raise UneededThumbnail()
if item:
item.status = UploadStatus.CreatingThumbnail
if not small: if not small:
thumb.thumbnail((800, 600), PILImage.LANCZOS) thumb.thumbnail((800, 600), PILImage.LANCZOS)
@ -464,11 +483,17 @@ class MatrixClient(nio.AsyncClient):
data = out.getvalue() data = out.getvalue()
if encrypt: if encrypt:
if item:
item.status = UploadStatus.EncryptingThumbnail
data, crypt_dict = await self.encrypt_attachment(data) data, crypt_dict = await self.encrypt_attachment(data)
upload_mime = "application/octet-stream" upload_mime = "application/octet-stream"
else: else:
crypt_dict, upload_mime = {}, mime crypt_dict, upload_mime = {}, mime
if item:
item.status = UploadStatus.UploadingThumbnail
return ( return (
await self.upload(data, upload_mime, Path(path).name), await self.upload(data, upload_mime, Path(path).name),
{ {
@ -485,8 +510,13 @@ class MatrixClient(nio.AsyncClient):
raise UnthumbnailableError(err) raise UnthumbnailableError(err)
async def upload_file(self, path: Union[Path, str], encrypt: bool = False, async def upload_file(
self,
path: Union[Path, str],
item: Optional[Upload] = None,
encrypt: bool = False,
) -> Tuple[str, str, CryptDict]: ) -> Tuple[str, str, CryptDict]:
with open(path, "rb") as file: with open(path, "rb") as file:
mime = utils.guess_mime(file) mime = utils.guess_mime(file)
file.seek(0, 0) file.seek(0, 0)
@ -494,11 +524,17 @@ class MatrixClient(nio.AsyncClient):
data: Union[BinaryIO, bytes] data: Union[BinaryIO, bytes]
if encrypt: if encrypt:
if item:
item.status = UploadStatus.Encrypting
data, crypt_dict = await self.encrypt_attachment(file.read()) data, crypt_dict = await self.encrypt_attachment(file.read())
upload_mime = "application/octet-stream" upload_mime = "application/octet-stream"
else: else:
data, crypt_dict, upload_mime = file, {}, mime data, crypt_dict, upload_mime = file, {}, mime
if item:
item.status = UploadStatus.Uploading
return ( return (
await self.upload(data, upload_mime, Path(path).name), await self.upload(data, upload_mime, Path(path).name),
mime, mime,

View File

@ -1,7 +1,9 @@
import re import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from uuid import uuid4
import lxml # nosec import lxml # nosec
@ -101,6 +103,38 @@ class Member(ModelItem):
return self.display_name return self.display_name
class UploadStatus(AutoStrEnum):
Starting = auto()
Encrypting = auto()
Uploading = auto()
CreatingThumbnail = auto()
EncryptingThumbnail = auto()
UploadingThumbnail = auto()
Success = auto()
Failure = auto() # TODO
@dataclass
class Upload(ModelItem):
filepath: str = field()
status: UploadStatus = UploadStatus.Starting
total_size: int = 0
uploaded: int = 0
uuid: str = field(init=False, default_factory=lambda: str(uuid4()))
start_date: datetime = field(init=False, default_factory=datetime.now)
def __post_init__(self) -> None:
if not self.total_size:
self.total_size = Path(self.filepath).resolve().stat().st_size
def __lt__(self, other: "Upload") -> bool:
# TODO
return self.start_date > other.start_date
class TypeSpecifier(AutoStrEnum): class TypeSpecifier(AutoStrEnum):
none = auto() none = auto()
profile_change = auto() profile_change = auto()

View File

@ -25,6 +25,10 @@ HSplitView {
Layout.fillWidth: true Layout.fillWidth: true
} }
UploadsBar {
Layout.fillWidth: true
}
InviteBanner { InviteBanner {
id: inviteBanner id: inviteBanner
visible: ! chatPage.roomInfo.left && inviterId visible: ! chatPage.roomInfo.left && inviterId

105
src/qml/Chat/UploadsBar.qml Normal file
View File

@ -0,0 +1,105 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import "../Base"
import "../utils.js" as Utils
Rectangle {
id: uploadsBar
implicitWidth: 800
implicitHeight: firstDelegate ? firstDelegate.height : 0
color: theme.chat.typingMembers.background
opacity: implicitHeight ? 1 : 0
property int delegateHeight: 0
property int maxShownDelegates: 1
readonly property var firstDelegate:
uploadsList.contentItem.visibleChildren[0]
Behavior on implicitHeight { HNumberAnimation {} }
HListView {
id: uploadsList
enableFlicking: false
width: parent.width
model: HListModel {
keyField: "uuid"
source: modelSources[["Upload", chatPage.roomId]] || []
}
delegate: HColumnLayout {
id: delegate
width: uploadsList.width
Component.onCompleted: Utils.debug(delegate)
HRowLayout {
HLabel {
id: filenameLabel
elide: Text.ElideRight
text:
model.status === "Starting" ?
qsTr("Preparing %1...").arg(fileName) :
model.status === "Encrypting" ?
qsTr("Encrypting %1...").arg(fileName) :
model.status === "Uploading" ?
qsTr("Uploading %1...").arg(fileName) :
model.status === "CreatingThumbnail" ?
qsTr("Generating thumbnail for %1...").arg(fileName) :
model.status === "EncryptingThumbnail" ?
qsTr("Encrypting thumbnail for %1...").arg(fileName) :
model.status === "UploadingThumbnail" ?
qsTr("Uploading thumbnail for %1...").arg(fileName) :
model.status === "Failure" ?
qsTr("Failed uploading %1.").arg(fileName) :
qsTr("Invalid status for %1: %2")
.arg(fileName, model.status)
topPadding: theme.spacing / 2
bottomPadding: topPadding
leftPadding: theme.spacing / 1.5
rightPadding: leftPadding
Layout.fillWidth: true
readonly property string fileName:
model.filepath.split("/").slice(-1)[0]
}
HSpacer {}
HLabel {
id: uploadCountLabel
visible: Layout.preferredWidth > 0
text: qsTr("%1/%2")
.arg(model.index + 1).arg(uploadsList.model.count)
topPadding: theme.spacing / 2
bottomPadding: topPadding
leftPadding: theme.spacing / 1.5
rightPadding: leftPadding
Layout.preferredWidth:
uploadsList.model.count < 2 ? 0 : implicitWidth
Behavior on Layout.preferredWidth { HNumberAnimation {} }
}
}
HProgressBar {
id: progressBar
Layout.fillWidth: true
}
}
}
}