From e017dd42637071687f88f5a36e7e03f1536332be Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 28 Dec 2018 12:53:01 +0900 Subject: FileTransferInfo: new properties: isUpload and started Also: use constructors instead of list-based initialisation in FileTransferPrivateInfo to enable a case of "invalid/empty" FileTransferPrivateInfo with status == None. --- lib/room.h | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'lib/room.h') diff --git a/lib/room.h b/lib/room.h index 6384b706..b832977f 100644 --- a/lib/room.h +++ b/lib/room.h @@ -42,10 +42,17 @@ namespace QMatrixClient class SetRoomStateWithKeyJob; class RedactEventJob; + /** The data structure used to expose file transfer information to views + * + * This is specifically tuned to work with QML exposing all traits as + * Q_PROPERTY values. + */ class FileTransferInfo { Q_GADGET + Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT) Q_PROPERTY(bool active READ active CONSTANT) + Q_PROPERTY(bool started READ started CONSTANT) Q_PROPERTY(bool completed READ completed CONSTANT) Q_PROPERTY(bool failed READ failed CONSTANT) Q_PROPERTY(int progress MEMBER progress CONSTANT) @@ -55,14 +62,15 @@ namespace QMatrixClient public: enum Status { None, Started, Completed, Failed }; Status status = None; + bool isUpload = false; int progress = 0; int total = -1; QUrl localDir { }; QUrl localPath { }; - bool active() const - { return status == Started || status == Completed; } + bool started() const { return status == Started; } bool completed() const { return status == Completed; } + bool active() const { return started() || completed(); } bool failed() const { return status == Failed; } }; -- cgit v1.2.3 From 143fffcf3962184befbbe37bebc5544d25bc7c39 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 28 Dec 2018 12:55:21 +0900 Subject: Room::fileSource Also: const'ified other methods related to file urls. --- lib/room.cpp | 21 ++++++++++++++++++--- lib/room.h | 7 ++++--- 2 files changed, 22 insertions(+), 6 deletions(-) (limited to 'lib/room.h') diff --git a/lib/room.cpp b/lib/room.cpp index d613fd77..23bbbc5b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -911,7 +911,7 @@ QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const return fileName; } -QUrl Room::urlToThumbnail(const QString& eventId) +QUrl Room::urlToThumbnail(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) if (event->hasThumbnail()) @@ -925,7 +925,7 @@ QUrl Room::urlToThumbnail(const QString& eventId) return {}; } -QUrl Room::urlToDownload(const QString& eventId) +QUrl Room::urlToDownload(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) { @@ -937,7 +937,7 @@ QUrl Room::urlToDownload(const QString& eventId) return {}; } -QString Room::fileNameToDownload(const QString& eventId) +QString Room::fileNameToDownload(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) return d->fileNameToDownload(event); @@ -978,6 +978,21 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const #endif } +QUrl Room::fileSource(const QString& id) const +{ + auto url = urlToDownload(id); + if (url.isValid()) + return url; + + // No urlToDownload means it's a pending or completed upload. + auto infoIt = d->fileTransfers.find(id); + if (infoIt != d->fileTransfers.end()) + return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); + + qCWarning(MAIN) << "File source for identifier" << id << "not found"; + return {}; +} + QString Room::prettyPrint(const QString& plainText) const { return QMatrixClient::prettyPrint(plainText); diff --git a/lib/room.h b/lib/room.h index b832977f..a166be37 100644 --- a/lib/room.h +++ b/lib/room.h @@ -342,10 +342,11 @@ namespace QMatrixClient /// Get the list of users this room is a direct chat with QList directChatUsers() const; - Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId); - Q_INVOKABLE QUrl urlToDownload(const QString& eventId); - Q_INVOKABLE QString fileNameToDownload(const QString& eventId); + Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const; + Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const; + Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const; Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; + Q_INVOKABLE QUrl fileSource(const QString& id) const; /** Pretty-prints plain text into HTML * As of now, it's exactly the same as QMatrixClient::prettyPrint(); -- cgit v1.2.3 From 3dcf0d3fd1e64d64b57976e400888fe8a02c4451 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 28 Dec 2018 15:00:08 +0900 Subject: Room::postFile() and supplementary things in Room::Private --- lib/room.cpp | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- lib/room.h | 1 + 2 files changed, 54 insertions(+), 5 deletions(-) (limited to 'lib/room.h') diff --git a/lib/room.cpp b/lib/room.cpp index 23bbbc5b..6500366e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include @@ -67,9 +68,11 @@ using std::llround; enum EventsPlacement : int { Older = -1, Newer = 1 }; -// A workaround for MSVC 2015 that fails with "error C2440: 'return': -// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'" -#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4) +// A workaround for MSVC 2015 and older GCC's that don't handle initializer +// lists right (MSVC 2015, notably, fails with "error C2440: 'return': +// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'") +#if (defined(_MSC_VER) && _MSC_VER < 1910) || \ + (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 4) # define WORKAROUND_EXTENDED_INITIALIZER_LIST #endif @@ -236,6 +239,8 @@ class Room::Private return sendEvent(makeEvent(std::forward(eventArgs)...)); } + RoomEvent* addAsPending(RoomEventPtr&& event); + QString doSendEvent(const RoomEvent* pEvent); PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr); void onEventSendingFailure(const RoomEvent* pEvent, @@ -1272,7 +1277,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) } } -QString Room::Private::sendEvent(RoomEventPtr&& event) +RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) { if (event->transactionId().isEmpty()) event->setTransactionId(connection->generateTxnId()); @@ -1280,7 +1285,12 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) emit q->pendingEventAboutToAdd(pEvent); unsyncedEvents.emplace_back(move(event)); emit q->pendingEventAdded(); - return doSendEvent(pEvent); + return pEvent; +} + +QString Room::Private::sendEvent(RoomEventPtr&& event) +{ + return doSendEvent(addAsPending(std::move(event))); } QString Room::Private::doSendEvent(const RoomEvent* pEvent) @@ -1357,6 +1367,7 @@ QString Room::retryMessage(const QString& txnId) [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Retrying transaction" << txnId; + // TODO: Support retrying uploads it->resetStatus(); return d->doSendEvent(it->event()); } @@ -1367,6 +1378,7 @@ void Room::discardMessage(const QString& txnId) [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Discarding transaction" << txnId; + // TODO: Discard an ongoing upload if there is any emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin()); d->unsyncedEvents.erase(it); emit pendingEventDiscarded(); @@ -1394,6 +1406,42 @@ QString Room::postHtmlText(const QString& plainText, const QString& html) return postHtmlMessage(plainText, html, MessageEventType::Text); } +QString Room::postFile(const QString& plainText, const QUrl& localPath) +{ + QFileInfo localFile { localPath.toLocalFile() }; + Q_ASSERT(localFile.isFile()); + // Remote URL will only be known after upload, see below. + auto* content = new EventContent::FileContent(QUrl(), localFile.size(), + QMimeDatabase().mimeTypeForUrl(localPath)); + // TODO: Set the msgtype based on MIME type + auto* pEvent = d->addAsPending( + makeEvent(plainText, "m.file", content)); + const auto txnId = pEvent->transactionId(); + uploadFile(txnId, localPath); + QMetaObject::Connection c; + c = connect(this, &Room::fileTransferCompleted, this, + [c,this,pEvent,txnId] (const QString& id, QUrl, const QUrl& mxcUri) { + if (id == txnId) + { + auto it = d->findAsPending(pEvent); + if (it != d->unsyncedEvents.end()) + { + it->setFileUploaded(mxcUri); + emit pendingEventChanged(it - d->unsyncedEvents.begin()); + d->doSendEvent(pEvent); + } else { + // Normally in this situation we should instruct + // the media server to delete the file; alas, there's no + // API specced for that. + qCWarning(MAIN) << "File uploaded to" << mxcUri + << "but the event referring to it was cancelled"; + } + disconnect(c); + } + }); + return txnId; +} + QString Room::postEvent(RoomEvent* event) { if (usesEncryption()) diff --git a/lib/room.h b/lib/room.h index a166be37..ce1c3dd3 100644 --- a/lib/room.h +++ b/lib/room.h @@ -374,6 +374,7 @@ namespace QMatrixClient QString postHtmlMessage(const QString& plainText, const QString& html, MessageEventType type); QString postHtmlText(const QString& plainText, const QString& html); + QString postFile(const QString& plainText, const QUrl& localPath); /** Post a pre-created room message event * * Takes ownership of the event, deleting it once the matching one -- cgit v1.2.3 From e3c1b93483eafbb94f1224b57e562984f4100538 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 29 Dec 2018 23:12:46 +0900 Subject: Support file events in Room::retryMessage/discardMessage --- lib/room.cpp | 43 +++++++++++++++++++++++++++++++++++++++++-- lib/room.h | 2 +- 2 files changed, 42 insertions(+), 3 deletions(-) (limited to 'lib/room.h') diff --git a/lib/room.cpp b/lib/room.cpp index 6500366e..2c90955e 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1367,7 +1367,32 @@ QString Room::retryMessage(const QString& txnId) [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Retrying transaction" << txnId; - // TODO: Support retrying uploads + const auto& transferIt = d->fileTransfers.find(txnId); + if (transferIt != d->fileTransfers.end()) + { + Q_ASSERT(transferIt->isUpload); + if (transferIt->status == FileTransferInfo::Completed) + { + qCDebug(MAIN) << "File for transaction" << txnId + << "has already been uploaded, bypassing re-upload"; + } else { + if (isJobRunning(transferIt->job)) + { + qCDebug(MAIN) << "Abandoning the upload job for transaction" + << txnId << "and starting again"; + transferIt->job->abandon(); + emit fileTransferFailed(txnId, tr("File upload will be retried")); + } + uploadFile(txnId, + QUrl::fromLocalFile(transferIt->localFileInfo.absoluteFilePath())); + // FIXME: Content type is no more passed here but it should + } + } + if (it->deliveryStatus() == EventStatus::ReachedServer) + { + qCWarning(MAIN) << "The previous attempt has reached the server; two" + " events are likely to be in the timeline after retry"; + } it->resetStatus(); return d->doSendEvent(it->event()); } @@ -1378,7 +1403,21 @@ void Room::discardMessage(const QString& txnId) [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Discarding transaction" << txnId; - // TODO: Discard an ongoing upload if there is any + const auto& transferIt = d->fileTransfers.find(txnId); + if (transferIt != d->fileTransfers.end()) + { + Q_ASSERT(transferIt->isUpload); + if (isJobRunning(transferIt->job)) + { + transferIt->status = FileTransferInfo::Cancelled; + transferIt->job->abandon(); + emit fileTransferFailed(txnId, tr("File upload cancelled")); + } else if (transferIt->status == FileTransferInfo::Completed) + { + qCWarning(MAIN) << "File for transaction" << txnId + << "has been uploaded but the message was discarded"; + } + } emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin()); d->unsyncedEvents.erase(it); emit pendingEventDiscarded(); diff --git a/lib/room.h b/lib/room.h index ce1c3dd3..2b6a2172 100644 --- a/lib/room.h +++ b/lib/room.h @@ -60,7 +60,7 @@ namespace QMatrixClient Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT) Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT) public: - enum Status { None, Started, Completed, Failed }; + enum Status { None, Started, Completed, Failed, Cancelled }; Status status = None; bool isUpload = false; int progress = 0; -- cgit v1.2.3 From fb46c2d2a6e53557452837c2690f32a56387fcac Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Thu, 3 Jan 2019 22:28:09 +0900 Subject: Room: findPendingEvent; fixes for postFile() --- lib/room.cpp | 75 ++++++++++++++++++++++++++++++++++++++++++++---------------- lib/room.h | 5 +++- 2 files changed, 59 insertions(+), 21 deletions(-) (limited to 'lib/room.h') diff --git a/lib/room.cpp b/lib/room.cpp index 2c90955e..8f50607f 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -599,6 +599,19 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const return timelineEdge(); } +Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId) +{ + return std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), + [txnId] (const auto& item) { return item->transactionId() == txnId; }); +} + +Room::PendingEvents::const_iterator +Room::findPendingEvent(const QString& txnId) const +{ + return std::find_if(d->unsyncedEvents.cbegin(), d->unsyncedEvents.cend(), + [txnId] (const auto& item) { return item->transactionId() == txnId; }); +} + void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. @@ -1363,8 +1376,7 @@ void Room::Private::onEventSendingFailure(const RoomEvent* pEvent, QString Room::retryMessage(const QString& txnId) { - auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), - [txnId] (const auto& evt) { return evt->transactionId() == txnId; }); + const auto it = findPendingEvent(txnId); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Retrying transaction" << txnId; const auto& transferIt = d->fileTransfers.find(txnId); @@ -1418,7 +1430,7 @@ void Room::discardMessage(const QString& txnId) << "has been uploaded but the message was discarded"; } } - emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin()); + emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin())); d->unsyncedEvents.erase(it); emit pendingEventDiscarded(); } @@ -1445,28 +1457,29 @@ QString Room::postHtmlText(const QString& plainText, const QString& html) return postHtmlMessage(plainText, html, MessageEventType::Text); } -QString Room::postFile(const QString& plainText, const QUrl& localPath) +QString Room::postFile(const QString& plainText, const QUrl& localPath, + bool asGenericFile) { QFileInfo localFile { localPath.toLocalFile() }; Q_ASSERT(localFile.isFile()); - // Remote URL will only be known after upload, see below. - auto* content = new EventContent::FileContent(QUrl(), localFile.size(), - QMimeDatabase().mimeTypeForUrl(localPath)); - // TODO: Set the msgtype based on MIME type + // Remote URL will only be known after upload; fill in the local path + // to enable the preview while the event is pending. auto* pEvent = d->addAsPending( - makeEvent(plainText, "m.file", content)); + makeEvent(plainText, localFile, asGenericFile)); const auto txnId = pEvent->transactionId(); uploadFile(txnId, localPath); - QMetaObject::Connection c; - c = connect(this, &Room::fileTransferCompleted, this, - [c,this,pEvent,txnId] (const QString& id, QUrl, const QUrl& mxcUri) { + QMetaObject::Connection cCompleted, cCancelled; + cCompleted = connect(this, &Room::fileTransferCompleted, this, + [cCompleted,cCancelled,this,pEvent,txnId] + (const QString& id, QUrl, const QUrl& mxcUri) { if (id == txnId) { auto it = d->findAsPending(pEvent); if (it != d->unsyncedEvents.end()) { it->setFileUploaded(mxcUri); - emit pendingEventChanged(it - d->unsyncedEvents.begin()); + emit pendingEventChanged( + int(it - d->unsyncedEvents.begin())); d->doSendEvent(pEvent); } else { // Normally in this situation we should instruct @@ -1475,9 +1488,27 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath) qCWarning(MAIN) << "File uploaded to" << mxcUri << "but the event referring to it was cancelled"; } - disconnect(c); + disconnect(cCompleted); + disconnect(cCancelled); } }); + cCancelled = connect(this, &Room::fileTransferCancelled, this, + [cCompleted,cCancelled,this,pEvent,txnId] (const QString& id) { + if (id == txnId) + { + auto it = d->findAsPending(pEvent); + if (it != d->unsyncedEvents.end()) + { + emit pendingEventAboutToDiscard( + int(it - d->unsyncedEvents.begin())); + d->unsyncedEvents.erase(it); + emit pendingEventDiscarded(); + } + disconnect(cCompleted); + disconnect(cCancelled); + } + }); + return txnId; } @@ -1659,8 +1690,8 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) if (ongoingTransfer != d->fileTransfers.end() && ongoingTransfer->status == FileTransferInfo::Started) { - qCWarning(MAIN) << "Download for" << eventId - << "already started; to restart, cancel it first"; + qCWarning(MAIN) << "Transfer for" << eventId + << "is ongoing; download won't start"; return; } @@ -1923,11 +1954,15 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) break; it = nextPending + 1; - emit q->pendingEventAboutToMerge(nextPending->get(), - nextPendingPair.second - unsyncedEvents.begin()); + auto* nextPendingEvt = nextPending->get(); + emit q->pendingEventAboutToMerge(nextPendingEvt, + int(nextPendingPair.second - unsyncedEvents.begin())); qDebug(EVENTS) << "Merging pending event from transaction" - << (*nextPending)->transactionId() << "into" - << (*nextPending)->id(); + << nextPendingEvt->transactionId() << "into" + << nextPendingEvt->id(); + auto transfer = fileTransfers.take(nextPendingEvt->transactionId()); + if (transfer.status != FileTransferInfo::None) + fileTransfers.insert(nextPendingEvt->id(), transfer); unsyncedEvents.erase(nextPendingPair.second); if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer)) { diff --git a/lib/room.h b/lib/room.h index 2b6a2172..029f87b7 100644 --- a/lib/room.h +++ b/lib/room.h @@ -234,6 +234,8 @@ namespace QMatrixClient rev_iter_t findInTimeline(TimelineItem::index_t index) const; rev_iter_t findInTimeline(const QString& evtId) const; + PendingEvents::iterator findPendingEvent(const QString & txnId); + PendingEvents::const_iterator findPendingEvent(const QString & txnId) const; bool displayed() const; /// Mark the room as currently displayed to the user @@ -374,7 +376,8 @@ namespace QMatrixClient QString postHtmlMessage(const QString& plainText, const QString& html, MessageEventType type); QString postHtmlText(const QString& plainText, const QString& html); - QString postFile(const QString& plainText, const QUrl& localPath); + QString postFile(const QString& plainText, const QUrl& localPath, + bool asGenericFile = false); /** Post a pre-created room message event * * Takes ownership of the event, deleting it once the matching one -- cgit v1.2.3