Skip to content

Commit 5af4e78

Browse files
authored
Merge #711(kitsune): Tweak Avatar::Private::get()
2 parents 1289ef4 + 497c250 commit 5af4e78

File tree

1 file changed

+71
-43
lines changed

1 file changed

+71
-43
lines changed

Quotient/avatar.cpp

+71-43
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
#include "connection.h"
77
#include "logging_categories_p.h"
88

9-
#include "events/eventcontent.h"
109
#include "jobs/mediathumbnailjob.h"
1110

1211
#include <QtCore/QDir>
@@ -16,21 +15,22 @@
1615
#include <QtGui/QPainter>
1716

1817
using namespace Quotient;
19-
using std::move;
2018

21-
class Q_DECL_HIDDEN Avatar::Private {
19+
class Q_DECL_HIDDEN Avatar::Private : public QObject {
2220
public:
2321
explicit Private(QUrl url = {}) : _url(std::move(url)) {}
24-
~Private()
22+
~Private() override
2523
{
2624
if (isJobPending(_thumbnailRequest))
2725
_thumbnailRequest->abandon();
2826
if (isJobPending(_uploadRequest))
2927
_uploadRequest->abandon();
3028
}
29+
Q_DISABLE_COPY_MOVE(Private)
3130

3231
QImage get(Connection* connection, QSize size,
3332
get_callback_t callback) const;
33+
void thumbnailRequestFinished();
3434
bool upload(UploadContentJob* job, upload_callback_t&& callback);
3535

3636
bool checkUrl(const QUrl& url) const;
@@ -41,16 +41,15 @@ class Q_DECL_HIDDEN Avatar::Private {
4141
// The below are related to image caching, hence mutable
4242
mutable QImage _originalImage;
4343
mutable std::vector<std::pair<QSize, QImage>> _scaledImages;
44-
mutable QSize _requestedSize;
45-
mutable enum { Unknown, Cache, Network, Banned } _imageSource = Unknown;
44+
mutable QSize _largestRequestedSize{};
45+
enum ImageSource : quint8 { Unknown, Cache, Network, Invalid };
46+
mutable ImageSource _imageSource = Unknown;
4647
mutable QPointer<MediaThumbnailJob> _thumbnailRequest = nullptr;
4748
mutable QPointer<BaseJob> _uploadRequest = nullptr;
48-
mutable std::vector<get_callback_t> callbacks;
49+
mutable std::vector<get_callback_t> callbacks{};
4950
};
5051

51-
Avatar::Avatar()
52-
: d(makeImpl<Private>())
53-
{}
52+
Avatar::Avatar() : d(makeImpl<Private>()) {}
5453

5554
Avatar::Avatar(QUrl url) : d(makeImpl<Private>(std::move(url))) {}
5655

@@ -87,55 +86,82 @@ QString Avatar::mediaId() const { return d->_url.authority() + d->_url.path(); }
8786
QImage Avatar::Private::get(Connection* connection, QSize size,
8887
get_callback_t callback) const
8988
{
90-
if (!callback) {
91-
qCCritical(MAIN) << "Null callbacks are not allowed in Avatar::get";
92-
Q_ASSERT(false);
93-
}
94-
9589
if (_imageSource == Unknown && _originalImage.load(localFile())) {
9690
_imageSource = Cache;
97-
_requestedSize = _originalImage.size();
91+
_largestRequestedSize = _originalImage.size();
9892
}
9993

100-
// Alternating between longer-width and longer-height requests is a sure way
101-
// to trick the below code into constantly getting another image from
102-
// the server because the existing one is alleged unsatisfactory.
103-
// Client authors can only blame themselves if they do so.
94+
// Assuming that all thumbnails for this avatar have the same aspect ratio,
95+
// it's enough for the image requested before to be large enough in at least
96+
// one dimension to be suitable for scaling down to the requested size;
97+
// therefore the new size has to be larger in both dimensions to warrant a
98+
// new request to the server
10499
if (((_imageSource == Unknown && !_thumbnailRequest)
105-
|| size.width() > _requestedSize.width()
106-
|| size.height() > _requestedSize.height())
100+
|| (size.width() > _largestRequestedSize.width()
101+
&& size.height() > _largestRequestedSize.height()))
107102
&& checkUrl(_url)) {
108103
qCDebug(MAIN) << "Getting avatar from" << _url.toString();
109-
_requestedSize = size;
104+
_largestRequestedSize = size;
110105
if (isJobPending(_thumbnailRequest))
111106
_thumbnailRequest->abandon();
112107
if (callback)
113108
callbacks.emplace_back(std::move(callback));
114109
_thumbnailRequest = connection->getThumbnail(_url, size);
115-
QObject::connect(_thumbnailRequest, &MediaThumbnailJob::success,
116-
_thumbnailRequest, [this] {
117-
_imageSource = Network;
118-
_originalImage = _thumbnailRequest->scaledThumbnail(
119-
_requestedSize);
120-
_originalImage.save(localFile());
121-
_scaledImages.clear();
122-
for (const auto& n : callbacks)
123-
n();
124-
callbacks.clear();
125-
});
110+
connect(_thumbnailRequest, &MediaThumbnailJob::finished, this,
111+
&Private::thumbnailRequestFinished);
112+
// The result of this request will only be returned when get() is
113+
// called next time afterwards
126114
}
115+
if (_imageSource == Invalid || _originalImage.isNull())
116+
return {};
127117

128-
for (const auto& [scaledSize, scaledImage] : _scaledImages)
129-
if (scaledSize == size)
118+
// NB: because of KeepAspectRatio, scaledImage.size() might not be equal to
119+
// requestedSize - this is why requestedSize is stored separately
120+
for (const auto& [requestedSize, scaledImage] : _scaledImages)
121+
if (requestedSize == size)
130122
return scaledImage;
131-
auto result = _originalImage.isNull()
132-
? QImage()
133-
: _originalImage.scaled(size, Qt::KeepAspectRatio,
134-
Qt::SmoothTransformation);
123+
124+
const auto& result = _originalImage.scaled(size, Qt::KeepAspectRatio,
125+
Qt::SmoothTransformation);
135126
_scaledImages.emplace_back(size, result);
136127
return result;
137128
}
138129

130+
void Avatar::Private::thumbnailRequestFinished()
131+
{
132+
// NB: The following code preserves _originalImage in case of
133+
// most errors
134+
switch (_thumbnailRequest->error()) {
135+
case BaseJob::NoError: break;
136+
case BaseJob::NetworkError:
137+
case BaseJob::NetworkAuthRequired:
138+
case BaseJob::TooManyRequests: // Shouldn't reach here but just in case
139+
case BaseJob::Timeout:
140+
return; // Make another attempt when requested again
141+
default:
142+
// Other errors are likely unrecoverable but just in case,
143+
// check if there's a previous image to fall back to; if
144+
// there is, assume that the error is temporary
145+
if (_originalImage.isNull())
146+
_imageSource = Invalid; // Can't do much with the rest
147+
return;
148+
}
149+
auto&& img = _thumbnailRequest->thumbnail();
150+
if (img.format() == QImage::Format_Invalid) {
151+
qCWarning(MAIN) << "The request for" << _url
152+
<< "was successful but the received image "
153+
"is invalid or unsupported";
154+
return;
155+
}
156+
_imageSource = Network;
157+
_originalImage = std::move(img);
158+
_originalImage.save(localFile());
159+
_scaledImages.clear();
160+
for (const auto& n : callbacks)
161+
n();
162+
callbacks.clear();
163+
}
164+
139165
bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t &&callback)
140166
{
141167
_uploadRequest = job;
@@ -148,17 +174,17 @@ bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t &&callback
148174

149175
bool Avatar::Private::checkUrl(const QUrl& url) const
150176
{
151-
if (_imageSource == Banned || url.isEmpty())
177+
if (_imageSource == Invalid || url.isEmpty())
152178
return false;
153179

154180
// FIXME: Make "mxc" a library-wide constant and maybe even make
155181
// the URL checker a Connection(?) method.
156182
if (!url.isValid() || url.scheme() != "mxc"_ls || url.path().count(u'/') != 1) {
157183
qCWarning(MAIN) << "Avatar URL is invalid or not mxc-based:"
158184
<< url.toDisplayString();
159-
_imageSource = Banned;
185+
_imageSource = Invalid;
160186
}
161-
return _imageSource != Banned;
187+
return _imageSource != Invalid;
162188
}
163189

164190
QString Avatar::Private::localFile() const
@@ -176,6 +202,8 @@ bool Avatar::updateUrl(const QUrl& newUrl)
176202

177203
d->_url = newUrl;
178204
d->_imageSource = Private::Unknown;
205+
d->_originalImage = {};
206+
d->_scaledImages.clear();
179207
if (isJobPending(d->_thumbnailRequest))
180208
d->_thumbnailRequest->abandon();
181209
return true;

0 commit comments

Comments
 (0)