mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-04-01 23:00:58 +00:00
Implement file reference update in streaming.
This commit is contained in:
parent
648cd44ddd
commit
c574119718
Telegram/SourceFiles
@ -361,8 +361,8 @@ public:
|
|||||||
|
|
||||||
AudioMsgId() = default;
|
AudioMsgId() = default;
|
||||||
AudioMsgId(
|
AudioMsgId(
|
||||||
DocumentData *audio,
|
not_null<DocumentData*> audio,
|
||||||
const FullMsgId &msgId,
|
FullMsgId msgId,
|
||||||
uint32 externalPlayId = 0)
|
uint32 externalPlayId = 0)
|
||||||
: _audio(audio)
|
: _audio(audio)
|
||||||
, _contextId(msgId)
|
, _contextId(msgId)
|
||||||
@ -373,21 +373,20 @@ public:
|
|||||||
[[nodiscard]] static uint32 CreateExternalPlayId();
|
[[nodiscard]] static uint32 CreateExternalPlayId();
|
||||||
[[nodiscard]] static AudioMsgId ForVideo();
|
[[nodiscard]] static AudioMsgId ForVideo();
|
||||||
|
|
||||||
Type type() const {
|
[[nodiscard]] Type type() const {
|
||||||
return _type;
|
return _type;
|
||||||
}
|
}
|
||||||
DocumentData *audio() const {
|
[[nodiscard]] DocumentData *audio() const {
|
||||||
return _audio;
|
return _audio;
|
||||||
}
|
}
|
||||||
FullMsgId contextId() const {
|
[[nodiscard]] FullMsgId contextId() const {
|
||||||
return _contextId;
|
return _contextId;
|
||||||
}
|
}
|
||||||
uint32 externalPlayId() const {
|
[[nodiscard]] uint32 externalPlayId() const {
|
||||||
return _externalPlayId;
|
return _externalPlayId;
|
||||||
}
|
}
|
||||||
|
[[nodiscard]] explicit operator bool() const {
|
||||||
explicit operator bool() const {
|
return (_audio != nullptr) || (_externalPlayId != 0);
|
||||||
return _audio != nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -711,11 +711,10 @@ void HistoryDocument::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool
|
|||||||
const auto type = AudioMsgId::Type::Voice;
|
const auto type = AudioMsgId::Type::Voice;
|
||||||
const auto state = Media::Player::instance()->getState(type);
|
const auto state = Media::Player::instance()->getState(type);
|
||||||
if (state.id == AudioMsgId(_data, _parent->data()->fullId(), state.id.externalPlayId()) && state.length) {
|
if (state.id == AudioMsgId(_data, _parent->data()->fullId(), state.id.externalPlayId()) && state.length) {
|
||||||
auto currentProgress = voice->seekingCurrent();
|
const auto currentProgress = voice->seekingCurrent();
|
||||||
auto currentPosition = state.frequency
|
Media::Player::instance()->finishSeeking(
|
||||||
? qRound(currentProgress * state.length * 1000. / state.frequency)
|
AudioMsgId::Type::Voice,
|
||||||
: 0;
|
currentProgress);
|
||||||
Media::Player::mixer()->seek(type, currentPosition);
|
|
||||||
|
|
||||||
voice->ensurePlayback(this);
|
voice->ensurePlayback(this);
|
||||||
voice->_playback->_position = 0;
|
voice->_playback->_position = 0;
|
||||||
|
@ -1242,7 +1242,9 @@ void Mixer::setStoppedState(Track *current, State state) {
|
|||||||
alSourceStop(current->stream.source);
|
alSourceStop(current->stream.source);
|
||||||
alSourcef(current->stream.source, AL_GAIN, 1);
|
alSourcef(current->stream.source, AL_GAIN, 1);
|
||||||
}
|
}
|
||||||
emit loaderOnCancel(current->state.id);
|
if (current->state.id) {
|
||||||
|
emit loaderOnCancel(current->state.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mixer::clearStoppedAtStart(const AudioMsgId &audio) {
|
void Mixer::clearStoppedAtStart(const AudioMsgId &audio) {
|
||||||
|
@ -413,6 +413,8 @@ Mixer::Track *Loaders::checkLoader(AudioMsgId::Type type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Loaders::onCancel(const AudioMsgId &audio) {
|
void Loaders::onCancel(const AudioMsgId &audio) {
|
||||||
|
Expects(audio.type() != AudioMsgId::Type::Unknown);
|
||||||
|
|
||||||
switch (audio.type()) {
|
switch (audio.type()) {
|
||||||
case AudioMsgId::Type::Voice: if (_audio == audio) clear(audio.type()); break;
|
case AudioMsgId::Type::Voice: if (_audio == audio) clear(audio.type()); break;
|
||||||
case AudioMsgId::Type::Song: if (_song == audio) clear(audio.type()); break;
|
case AudioMsgId::Type::Song: if (_song == audio) clear(audio.type()); break;
|
||||||
|
@ -112,10 +112,11 @@ Instance::Instance()
|
|||||||
Instance::~Instance() = default;
|
Instance::~Instance() = default;
|
||||||
|
|
||||||
AudioMsgId::Type Instance::getActiveType() const {
|
AudioMsgId::Type Instance::getActiveType() const {
|
||||||
auto voiceData = getData(AudioMsgId::Type::Voice);
|
const auto voiceData = getData(AudioMsgId::Type::Voice);
|
||||||
if (voiceData->current) {
|
if (voiceData->current) {
|
||||||
const auto state = getState(voiceData->type);
|
const auto state = getState(voiceData->type);
|
||||||
if (voiceData->current == state.id && !IsStoppedOrStopping(state.state)) {
|
if (voiceData->current == state.id
|
||||||
|
&& !IsStoppedOrStopping(state.state)) {
|
||||||
return voiceData->type;
|
return voiceData->type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,11 +150,9 @@ void Instance::setCurrent(const AudioMsgId &audioId) {
|
|||||||
data->current = audioId;
|
data->current = audioId;
|
||||||
data->isPlaying = false;
|
data->isPlaying = false;
|
||||||
|
|
||||||
auto history = data->history;
|
const auto history = data->history;
|
||||||
auto migrated = data->migrated;
|
const auto migrated = data->migrated;
|
||||||
auto item = data->current
|
const auto item = App::histItemById(data->current.contextId());
|
||||||
? App::histItemById(data->current.contextId())
|
|
||||||
: nullptr;
|
|
||||||
if (item) {
|
if (item) {
|
||||||
data->history = item->history()->migrateToOrMe();
|
data->history = item->history()->migrateToOrMe();
|
||||||
data->migrated = data->history->migrateFrom();
|
data->migrated = data->history->migrateFrom();
|
||||||
@ -333,7 +332,7 @@ void Instance::play(AudioMsgId::Type type) {
|
|||||||
} else {
|
} else {
|
||||||
mixer()->resume(state.id);
|
mixer()->resume(state.id);
|
||||||
}
|
}
|
||||||
} else if (data->current) {
|
} else {
|
||||||
play(data->current);
|
play(data->current);
|
||||||
}
|
}
|
||||||
data->resumeOnCallEnd = false;
|
data->resumeOnCallEnd = false;
|
||||||
@ -342,7 +341,7 @@ void Instance::play(AudioMsgId::Type type) {
|
|||||||
|
|
||||||
void Instance::play(const AudioMsgId &audioId) {
|
void Instance::play(const AudioMsgId &audioId) {
|
||||||
const auto document = audioId.audio();
|
const auto document = audioId.audio();
|
||||||
if (!audioId || !document) {
|
if (!document) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (document->isAudioFile()) {
|
if (document->isAudioFile()) {
|
||||||
@ -465,9 +464,7 @@ void Instance::playPause(AudioMsgId::Type type) {
|
|||||||
mixer()->pause(state.id);
|
mixer()->pause(state.id);
|
||||||
}
|
}
|
||||||
} else if (auto data = getData(type)) {
|
} else if (auto data = getData(type)) {
|
||||||
if (data->current) {
|
play(data->current);
|
||||||
play(data->current);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data->resumeOnCallEnd = false;
|
data->resumeOnCallEnd = false;
|
||||||
|
@ -99,6 +99,7 @@ void LoaderMtproto::sendNext() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static auto DcIndex = 0;
|
static auto DcIndex = 0;
|
||||||
|
const auto reference = locationFileReference();
|
||||||
const auto id = _sender.request(MTPupload_GetFile(
|
const auto id = _sender.request(MTPupload_GetFile(
|
||||||
_location,
|
_location,
|
||||||
MTP_int(offset),
|
MTP_int(offset),
|
||||||
@ -106,7 +107,7 @@ void LoaderMtproto::sendNext() {
|
|||||||
)).done([=](const MTPupload_File &result) {
|
)).done([=](const MTPupload_File &result) {
|
||||||
requestDone(offset, result);
|
requestDone(offset, result);
|
||||||
}).fail([=](const RPCError &error) {
|
}).fail([=](const RPCError &error) {
|
||||||
requestFailed(offset, error);
|
requestFailed(offset, error, reference);
|
||||||
}).toDC(
|
}).toDC(
|
||||||
MTP::downloadDcId(_dcId, (++DcIndex) % MTP::kDownloadSessionsCount)
|
MTP::downloadDcId(_dcId, (++DcIndex) % MTP::kDownloadSessionsCount)
|
||||||
).send();
|
).send();
|
||||||
@ -138,21 +139,57 @@ void LoaderMtproto::changeCdnParams(
|
|||||||
const QByteArray &encryptionKey,
|
const QByteArray &encryptionKey,
|
||||||
const QByteArray &encryptionIV,
|
const QByteArray &encryptionIV,
|
||||||
const QVector<MTPFileHash> &hashes) {
|
const QVector<MTPFileHash> &hashes) {
|
||||||
// #TODO streaming cdn
|
// #TODO streaming later cdn
|
||||||
|
_parts.fire({ LoadedPart::kFailedOffset });
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoaderMtproto::requestFailed(int offset, const RPCError &error) {
|
void LoaderMtproto::requestFailed(
|
||||||
|
int offset,
|
||||||
|
const RPCError &error,
|
||||||
|
const QByteArray &usedFileReference) {
|
||||||
const auto &type = error.type();
|
const auto &type = error.type();
|
||||||
if (error.code() != 400 || !type.startsWith(qstr("FILE_REFERENCE_"))) {
|
const auto fail = [=] {
|
||||||
_parts.fire({ LoadedPart::kFailedOffset });
|
_parts.fire({ LoadedPart::kFailedOffset });
|
||||||
return;
|
};
|
||||||
|
if (error.code() != 400 || !type.startsWith(qstr("FILE_REFERENCE_"))) {
|
||||||
|
return fail();
|
||||||
}
|
}
|
||||||
const auto callback = [=](const Data::UpdatedFileReferences &updated) {
|
const auto callback = [=](const Data::UpdatedFileReferences &updated) {
|
||||||
// #TODO streaming file_reference
|
_location.match([&](const MTPDinputDocumentFileLocation &location) {
|
||||||
|
const auto i = updated.data.find(location.vid.v);
|
||||||
|
if (i == end(updated.data)) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
const auto reference = i->second;
|
||||||
|
if (reference == usedFileReference) {
|
||||||
|
return fail();
|
||||||
|
} else if (reference != location.vfile_reference.v) {
|
||||||
|
_location = MTP_inputDocumentFileLocation(
|
||||||
|
MTP_long(location.vid.v),
|
||||||
|
MTP_long(location.vaccess_hash.v),
|
||||||
|
MTP_bytes(reference));
|
||||||
|
}
|
||||||
|
if (!_requests.take(offset)) {
|
||||||
|
// Request with such offset was already cancelled.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_requested.add(offset);
|
||||||
|
sendNext();
|
||||||
|
}, [](auto &&) {
|
||||||
|
Unexpected("Not implemented file location type.");
|
||||||
|
});
|
||||||
};
|
};
|
||||||
_api->refreshFileReference(_origin, crl::guard(this, callback));
|
_api->refreshFileReference(_origin, crl::guard(this, callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QByteArray LoaderMtproto::locationFileReference() const {
|
||||||
|
return _location.match([&](const MTPDinputDocumentFileLocation &data) {
|
||||||
|
return data.vfile_reference.v;
|
||||||
|
}, [](auto &&) -> QByteArray {
|
||||||
|
Unexpected("Not implemented file location type.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
rpl::producer<LoadedPart> LoaderMtproto::parts() const {
|
rpl::producer<LoadedPart> LoaderMtproto::parts() const {
|
||||||
return _parts.events();
|
return _parts.events();
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,10 @@ private:
|
|||||||
void sendNext();
|
void sendNext();
|
||||||
|
|
||||||
void requestDone(int offset, const MTPupload_File &result);
|
void requestDone(int offset, const MTPupload_File &result);
|
||||||
void requestFailed(int offset, const RPCError &error);
|
void requestFailed(
|
||||||
|
int offset,
|
||||||
|
const RPCError &error,
|
||||||
|
const QByteArray &usedFileReference);
|
||||||
void changeCdnParams(
|
void changeCdnParams(
|
||||||
int offset,
|
int offset,
|
||||||
MTP::DcId dcId,
|
MTP::DcId dcId,
|
||||||
@ -52,9 +55,14 @@ private:
|
|||||||
const QByteArray &encryptionIV,
|
const QByteArray &encryptionIV,
|
||||||
const QVector<MTPFileHash> &hashes);
|
const QVector<MTPFileHash> &hashes);
|
||||||
|
|
||||||
|
[[nodiscard]] QByteArray locationFileReference() const;
|
||||||
|
|
||||||
const not_null<ApiWrap*> _api;
|
const not_null<ApiWrap*> _api;
|
||||||
const MTP::DcId _dcId = 0;
|
const MTP::DcId _dcId = 0;
|
||||||
const MTPInputFileLocation _location;
|
|
||||||
|
// _location can be changed with an updated file_reference.
|
||||||
|
MTPInputFileLocation _location;
|
||||||
|
|
||||||
const int _size = 0;
|
const int _size = 0;
|
||||||
const Data::FileOrigin _origin;
|
const Data::FileOrigin _origin;
|
||||||
|
|
||||||
|
@ -218,7 +218,7 @@ void Player::fileReady(Stream &&video, Stream &&audio) {
|
|||||||
};
|
};
|
||||||
const auto mode = _options.mode;
|
const auto mode = _options.mode;
|
||||||
if (audio.codec && (mode == Mode::Audio || mode == Mode::Both)) {
|
if (audio.codec && (mode == Mode::Audio || mode == Mode::Both)) {
|
||||||
if (_options.audioId) {
|
if (_options.audioId.audio() != nullptr) {
|
||||||
_audioId = AudioMsgId(
|
_audioId = AudioMsgId(
|
||||||
_options.audioId.audio(),
|
_options.audioId.audio(),
|
||||||
_options.audioId.contextId(),
|
_options.audioId.contextId(),
|
||||||
|
@ -676,6 +676,39 @@ QRect OverlayWidget::contentRect() const {
|
|||||||
return { _x, _y, _w, _h };
|
return { _x, _y, _w, _h };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OverlayWidget::contentSizeChanged() {
|
||||||
|
_width = _w;
|
||||||
|
_height = _h;
|
||||||
|
if (_w > 0 && _h > 0) {
|
||||||
|
_zoomToScreen = float64(width()) / _w;
|
||||||
|
if (_h * _zoomToScreen > height()) {
|
||||||
|
_zoomToScreen = float64(height()) / _h;
|
||||||
|
}
|
||||||
|
if (_zoomToScreen >= 1.) {
|
||||||
|
_zoomToScreen -= 1.;
|
||||||
|
} else {
|
||||||
|
_zoomToScreen = 1. - (1. / _zoomToScreen);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_zoomToScreen = 0;
|
||||||
|
}
|
||||||
|
if ((_w > width()) || (_h > height()) || _fullScreenVideo) {
|
||||||
|
_zoom = ZoomToScreenLevel;
|
||||||
|
if (_zoomToScreen >= 0) {
|
||||||
|
_w = qRound(_w * (_zoomToScreen + 1));
|
||||||
|
_h = qRound(_h * (_zoomToScreen + 1));
|
||||||
|
} else {
|
||||||
|
_w = qRound(_w / (-_zoomToScreen + 1));
|
||||||
|
_h = qRound(_h / (-_zoomToScreen + 1));
|
||||||
|
}
|
||||||
|
snapXY();
|
||||||
|
} else {
|
||||||
|
_zoom = 0;
|
||||||
|
}
|
||||||
|
_x = (width() - _w) / 2;
|
||||||
|
_y = (height() - _h) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
float64 OverlayWidget::radialProgress() const {
|
float64 OverlayWidget::radialProgress() const {
|
||||||
if (_doc) {
|
if (_doc) {
|
||||||
return _doc->progress();
|
return _doc->progress();
|
||||||
@ -1645,18 +1678,7 @@ void OverlayWidget::displayPhoto(not_null<PhotoData*> photo, HistoryItem *item)
|
|||||||
if (isHidden()) {
|
if (isHidden()) {
|
||||||
moveToScreen();
|
moveToScreen();
|
||||||
}
|
}
|
||||||
if (_w > width()) {
|
contentSizeChanged();
|
||||||
_h = qRound(_h * width() / float64(_w));
|
|
||||||
_w = width();
|
|
||||||
}
|
|
||||||
if (_h > height()) {
|
|
||||||
_w = qRound(_w * height() / float64(_h));
|
|
||||||
_h = height();
|
|
||||||
}
|
|
||||||
_x = (width() - _w) / 2;
|
|
||||||
_y = (height() - _h) / 2;
|
|
||||||
_width = _w;
|
|
||||||
_height = _h;
|
|
||||||
if (_msgid && item) {
|
if (_msgid && item) {
|
||||||
_from = item->senderOriginal();
|
_from = item->senderOriginal();
|
||||||
} else {
|
} else {
|
||||||
@ -1798,36 +1820,7 @@ void OverlayWidget::displayDocument(DocumentData *doc, HistoryItem *item) {
|
|||||||
if (isHidden()) {
|
if (isHidden()) {
|
||||||
moveToScreen();
|
moveToScreen();
|
||||||
}
|
}
|
||||||
_width = _w;
|
contentSizeChanged();
|
||||||
_height = _h;
|
|
||||||
if (_w > 0 && _h > 0) {
|
|
||||||
_zoomToScreen = float64(width()) / _w;
|
|
||||||
if (_h * _zoomToScreen > height()) {
|
|
||||||
_zoomToScreen = float64(height()) / _h;
|
|
||||||
}
|
|
||||||
if (_zoomToScreen >= 1.) {
|
|
||||||
_zoomToScreen -= 1.;
|
|
||||||
} else {
|
|
||||||
_zoomToScreen = 1. - (1. / _zoomToScreen);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_zoomToScreen = 0;
|
|
||||||
}
|
|
||||||
if ((_w > width()) || (_h > height()) || _fullScreenVideo) {
|
|
||||||
_zoom = ZoomToScreenLevel;
|
|
||||||
if (_zoomToScreen >= 0) {
|
|
||||||
_w = qRound(_w * (_zoomToScreen + 1));
|
|
||||||
_h = qRound(_h * (_zoomToScreen + 1));
|
|
||||||
} else {
|
|
||||||
_w = qRound(_w / (-_zoomToScreen + 1));
|
|
||||||
_h = qRound(_h / (-_zoomToScreen + 1));
|
|
||||||
}
|
|
||||||
snapXY();
|
|
||||||
} else {
|
|
||||||
_zoom = 0;
|
|
||||||
}
|
|
||||||
_x = (width() - _w) / 2;
|
|
||||||
_y = (height() - _h) / 2;
|
|
||||||
if (_msgid && item) {
|
if (_msgid && item) {
|
||||||
_from = item->senderOriginal();
|
_from = item->senderOriginal();
|
||||||
} else {
|
} else {
|
||||||
@ -1927,6 +1920,19 @@ void OverlayWidget::initStreamingThumbnail() {
|
|||||||
_current.setDevicePixelRatio(cRetinaFactor());
|
_current.setDevicePixelRatio(cRetinaFactor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OverlayWidget::streamingReady(Streaming::Information &&info) {
|
||||||
|
_streamed->info = std::move(info);
|
||||||
|
validateStreamedGoodThumbnail();
|
||||||
|
if (videoShown()) {
|
||||||
|
const auto contentSize = ConvertScale(videoSize());
|
||||||
|
_w = contentSize.width();
|
||||||
|
_h = contentSize.height();
|
||||||
|
contentSizeChanged();
|
||||||
|
}
|
||||||
|
this->update(contentRect());
|
||||||
|
playbackWaitingChange(false);
|
||||||
|
}
|
||||||
|
|
||||||
void OverlayWidget::createStreamingObjects() {
|
void OverlayWidget::createStreamingObjects() {
|
||||||
_streamed = std::make_unique<Streamed>(
|
_streamed = std::make_unique<Streamed>(
|
||||||
&_doc->owner(),
|
&_doc->owner(),
|
||||||
@ -1978,10 +1984,7 @@ void OverlayWidget::handleStreamingUpdate(Streaming::Update &&update) {
|
|||||||
using namespace Streaming;
|
using namespace Streaming;
|
||||||
|
|
||||||
update.data.match([&](Information &update) {
|
update.data.match([&](Information &update) {
|
||||||
_streamed->info = std::move(update);
|
streamingReady(std::move(update));
|
||||||
validateStreamedGoodThumbnail();
|
|
||||||
this->update(contentRect());
|
|
||||||
playbackWaitingChange(false);
|
|
||||||
}, [&](const PreloadedVideo &update) {
|
}, [&](const PreloadedVideo &update) {
|
||||||
_streamed->info.video.state.receivedTill = update.till;
|
_streamed->info.video.state.receivedTill = update.till;
|
||||||
//updatePlaybackState();
|
//updatePlaybackState();
|
||||||
|
@ -37,6 +37,7 @@ namespace Player {
|
|||||||
struct TrackState;
|
struct TrackState;
|
||||||
} // namespace Player
|
} // namespace Player
|
||||||
namespace Streaming {
|
namespace Streaming {
|
||||||
|
struct Information;
|
||||||
struct Update;
|
struct Update;
|
||||||
struct Error;
|
struct Error;
|
||||||
} // namespace Streaming
|
} // namespace Streaming
|
||||||
@ -236,6 +237,7 @@ private:
|
|||||||
|
|
||||||
void initStreaming();
|
void initStreaming();
|
||||||
void initStreamingThumbnail();
|
void initStreamingThumbnail();
|
||||||
|
void streamingReady(Streaming::Information &&info);
|
||||||
void createStreamingObjects();
|
void createStreamingObjects();
|
||||||
void handleStreamingUpdate(Streaming::Update &&update);
|
void handleStreamingUpdate(Streaming::Update &&update);
|
||||||
void handleStreamingError(Streaming::Error &&error);
|
void handleStreamingError(Streaming::Error &&error);
|
||||||
@ -249,6 +251,7 @@ private:
|
|||||||
void changingMsgId(not_null<HistoryItem*> row, MsgId newId);
|
void changingMsgId(not_null<HistoryItem*> row, MsgId newId);
|
||||||
|
|
||||||
QRect contentRect() const;
|
QRect contentRect() const;
|
||||||
|
void contentSizeChanged();
|
||||||
|
|
||||||
// Radial animation interface.
|
// Radial animation interface.
|
||||||
float64 radialProgress() const;
|
float64 radialProgress() const;
|
||||||
|
Loading…
Reference in New Issue
Block a user