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
- SVG uploads
- Uploading progress bar (+local echo)
- Directly create cache files for our uploads before actually uploading
- Downloading

View File

@ -9,7 +9,7 @@ import nio
from .app import App
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
ProfileResponse = Union[nio.ProfileGetResponse, nio.ProfileGetError]
@ -29,6 +29,7 @@ class Backend:
(Device, str), # Devices of user_id
(Room, str), # Rooms for user_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
})

View File

@ -25,7 +25,9 @@ import nio
from . import __about__, utils
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 .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:
path = Path(path)
size = path.resolve().stat().st_size
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]
@ -234,7 +242,7 @@ class MatrixClient(nio.AsyncClient):
"body": path.name,
"info": {
"mimetype": mime,
"size": path.resolve().stat().st_size,
"size": size,
},
}
@ -254,7 +262,9 @@ class MatrixClient(nio.AsyncClient):
try:
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):
pass
else:
@ -302,6 +312,9 @@ class MatrixClient(nio.AsyncClient):
content["msgtype"] = "m.file"
content["filename"] = path.name
upload_item.status = UploadStatus.Success
del self.models[Upload, room_id]
uuid = str(uuid4())
await self._local_echo(
@ -435,7 +448,10 @@ class MatrixClient(nio.AsyncClient):
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]:
png_modes = ("1", "L", "P", "RGBA")
@ -450,6 +466,9 @@ class MatrixClient(nio.AsyncClient):
if small and is_jpg_png and not jpgable_png:
raise UneededThumbnail()
if item:
item.status = UploadStatus.CreatingThumbnail
if not small:
thumb.thumbnail((800, 600), PILImage.LANCZOS)
@ -464,11 +483,17 @@ class MatrixClient(nio.AsyncClient):
data = out.getvalue()
if encrypt:
if item:
item.status = UploadStatus.EncryptingThumbnail
data, crypt_dict = await self.encrypt_attachment(data)
upload_mime = "application/octet-stream"
else:
crypt_dict, upload_mime = {}, mime
if item:
item.status = UploadStatus.UploadingThumbnail
return (
await self.upload(data, upload_mime, Path(path).name),
{
@ -485,8 +510,13 @@ class MatrixClient(nio.AsyncClient):
raise UnthumbnailableError(err)
async def upload_file(self, path: Union[Path, str], encrypt: bool = False,
) -> Tuple[str, str, CryptDict]:
async def upload_file(
self,
path: Union[Path, str],
item: Optional[Upload] = None,
encrypt: bool = False,
) -> Tuple[str, str, CryptDict]:
with open(path, "rb") as file:
mime = utils.guess_mime(file)
file.seek(0, 0)
@ -494,11 +524,17 @@ class MatrixClient(nio.AsyncClient):
data: Union[BinaryIO, bytes]
if encrypt:
if item:
item.status = UploadStatus.Encrypting
data, crypt_dict = await self.encrypt_attachment(file.read())
upload_mime = "application/octet-stream"
else:
data, crypt_dict, upload_mime = file, {}, mime
if item:
item.status = UploadStatus.Uploading
return (
await self.upload(data, upload_mime, Path(path).name),
mime,

View File

@ -1,7 +1,9 @@
import re
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from uuid import uuid4
import lxml # nosec
@ -101,6 +103,38 @@ class Member(ModelItem):
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):
none = auto()
profile_change = auto()

View File

@ -25,6 +25,10 @@ HSplitView {
Layout.fillWidth: true
}
UploadsBar {
Layout.fillWidth: true
}
InviteBanner {
id: inviteBanner
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
}
}
}
}