6
6
#include " connection.h"
7
7
#include " logging_categories_p.h"
8
8
9
- #include " events/eventcontent.h"
10
9
#include " jobs/mediathumbnailjob.h"
11
10
12
11
#include < QtCore/QDir>
16
15
#include < QtGui/QPainter>
17
16
18
17
using namespace Quotient ;
19
- using std::move;
20
18
21
- class Q_DECL_HIDDEN Avatar::Private {
19
+ class Q_DECL_HIDDEN Avatar::Private : public QObject {
22
20
public:
23
21
explicit Private (QUrl url = {}) : _url(std::move(url)) {}
24
- ~Private ()
22
+ ~Private () override
25
23
{
26
24
if (isJobPending (_thumbnailRequest))
27
25
_thumbnailRequest->abandon ();
28
26
if (isJobPending (_uploadRequest))
29
27
_uploadRequest->abandon ();
30
28
}
29
+ Q_DISABLE_COPY_MOVE (Private)
31
30
32
31
QImage get (Connection* connection, QSize size,
33
32
get_callback_t callback) const ;
33
+ void thumbnailRequestFinished ();
34
34
bool upload (UploadContentJob* job, upload_callback_t && callback);
35
35
36
36
bool checkUrl (const QUrl& url) const ;
@@ -41,16 +41,15 @@ class Q_DECL_HIDDEN Avatar::Private {
41
41
// The below are related to image caching, hence mutable
42
42
mutable QImage _originalImage;
43
43
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;
46
47
mutable QPointer<MediaThumbnailJob> _thumbnailRequest = nullptr ;
47
48
mutable QPointer<BaseJob> _uploadRequest = nullptr ;
48
- mutable std::vector<get_callback_t > callbacks;
49
+ mutable std::vector<get_callback_t > callbacks{} ;
49
50
};
50
51
51
- Avatar::Avatar ()
52
- : d(makeImpl<Private>())
53
- {}
52
+ Avatar::Avatar () : d(makeImpl<Private>()) {}
54
53
55
54
Avatar::Avatar (QUrl url) : d(makeImpl<Private>(std::move(url))) {}
56
55
@@ -87,55 +86,82 @@ QString Avatar::mediaId() const { return d->_url.authority() + d->_url.path(); }
87
86
QImage Avatar::Private::get (Connection* connection, QSize size,
88
87
get_callback_t callback) const
89
88
{
90
- if (!callback) {
91
- qCCritical (MAIN) << " Null callbacks are not allowed in Avatar::get" ;
92
- Q_ASSERT (false );
93
- }
94
-
95
89
if (_imageSource == Unknown && _originalImage.load (localFile ())) {
96
90
_imageSource = Cache;
97
- _requestedSize = _originalImage.size ();
91
+ _largestRequestedSize = _originalImage.size ();
98
92
}
99
93
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
104
99
if (((_imageSource == Unknown && !_thumbnailRequest)
105
- || size.width () > _requestedSize .width ()
106
- || size.height () > _requestedSize .height ())
100
+ || ( size.width () > _largestRequestedSize .width ()
101
+ && size.height () > _largestRequestedSize .height () ))
107
102
&& checkUrl (_url)) {
108
103
qCDebug (MAIN) << " Getting avatar from" << _url.toString ();
109
- _requestedSize = size;
104
+ _largestRequestedSize = size;
110
105
if (isJobPending (_thumbnailRequest))
111
106
_thumbnailRequest->abandon ();
112
107
if (callback)
113
108
callbacks.emplace_back (std::move (callback));
114
109
_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
126
114
}
115
+ if (_imageSource == Invalid || _originalImage.isNull ())
116
+ return {};
127
117
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)
130
122
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);
135
126
_scaledImages.emplace_back (size, result);
136
127
return result;
137
128
}
138
129
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
+
139
165
bool Avatar::Private::upload (UploadContentJob* job, upload_callback_t &&callback)
140
166
{
141
167
_uploadRequest = job;
@@ -148,17 +174,17 @@ bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t &&callback
148
174
149
175
bool Avatar::Private::checkUrl (const QUrl& url) const
150
176
{
151
- if (_imageSource == Banned || url.isEmpty ())
177
+ if (_imageSource == Invalid || url.isEmpty ())
152
178
return false ;
153
179
154
180
// FIXME: Make "mxc" a library-wide constant and maybe even make
155
181
// the URL checker a Connection(?) method.
156
182
if (!url.isValid () || url.scheme () != " mxc" _ls || url.path ().count (u' /' ) != 1 ) {
157
183
qCWarning (MAIN) << " Avatar URL is invalid or not mxc-based:"
158
184
<< url.toDisplayString ();
159
- _imageSource = Banned ;
185
+ _imageSource = Invalid ;
160
186
}
161
- return _imageSource != Banned ;
187
+ return _imageSource != Invalid ;
162
188
}
163
189
164
190
QString Avatar::Private::localFile () const
@@ -176,6 +202,8 @@ bool Avatar::updateUrl(const QUrl& newUrl)
176
202
177
203
d->_url = newUrl;
178
204
d->_imageSource = Private::Unknown;
205
+ d->_originalImage = {};
206
+ d->_scaledImages .clear ();
179
207
if (isJobPending (d->_thumbnailRequest ))
180
208
d->_thumbnailRequest ->abandon ();
181
209
return true ;
0 commit comments