mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-09-05 00:46:08 +00:00
Edit media captions in message field.
This commit is contained in:
@@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_session_settings.h"
|
||||
#include "mainwidget.h" // controller->content() -> QWidget*
|
||||
#include "mtproto/mtproto_config.h"
|
||||
#include "platform/platform_specific.h"
|
||||
#include "storage/localimageloader.h" // SendMediaType
|
||||
@@ -69,7 +70,9 @@ namespace {
|
||||
|
||||
constexpr auto kChangesDebounceTimeout = crl::time(1000);
|
||||
|
||||
auto ListFromMimeData(not_null<const QMimeData*> data, bool premium) {
|
||||
[[nodiscard]] Ui::PreparedList ListFromMimeData(
|
||||
not_null<const QMimeData*> data,
|
||||
bool premium) {
|
||||
using Error = Ui::PreparedList::Error;
|
||||
const auto list = Core::ReadMimeUrls(data);
|
||||
auto result = !list.isEmpty()
|
||||
@@ -89,7 +92,7 @@ auto ListFromMimeData(not_null<const QMimeData*> data, bool premium) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Ui::AlbumType ComputeAlbumType(not_null<HistoryItem*> item) {
|
||||
[[nodiscard]] Ui::AlbumType ComputeAlbumType(not_null<HistoryItem*> item) {
|
||||
if (item->groupId().empty()) {
|
||||
return Ui::AlbumType();
|
||||
}
|
||||
@@ -109,17 +112,130 @@ Ui::AlbumType ComputeAlbumType(not_null<HistoryItem*> item) {
|
||||
return Ui::AlbumType();
|
||||
}
|
||||
|
||||
bool CanBeCompressed(Ui::AlbumType type) {
|
||||
[[nodiscard]] bool CanBeCompressed(Ui::AlbumType type) {
|
||||
return (type == Ui::AlbumType::None)
|
||||
|| (type == Ui::AlbumType::PhotoVideo);
|
||||
}
|
||||
|
||||
void ChooseReplacement(
|
||||
not_null<Window::SessionController*> controller,
|
||||
Ui::AlbumType type,
|
||||
Fn<void(Ui::PreparedList&&)> chosen) {
|
||||
const auto weak = base::make_weak(controller);
|
||||
const auto callback = [=](FileDialog::OpenResult &&result) {
|
||||
const auto strong = weak.get();
|
||||
if (!strong) {
|
||||
return;
|
||||
}
|
||||
const auto showError = [=](tr::phrase<> t) {
|
||||
if (const auto strong = weak.get()) {
|
||||
strong->showToast({ t(tr::now) });
|
||||
}
|
||||
};
|
||||
|
||||
const auto checkResult = [=](const Ui::PreparedList &list) {
|
||||
if (list.files.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
const auto &file = list.files.front();
|
||||
const auto mime = file.information->filemime;
|
||||
if (Core::IsMimeSticker(mime)) {
|
||||
showError(tr::lng_edit_media_invalid_file);
|
||||
return false;
|
||||
} else if (type != Ui::AlbumType::None
|
||||
&& !file.canBeInAlbumType(type)) {
|
||||
showError(tr::lng_edit_media_album_error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auto premium = strong->session().premium();
|
||||
auto list = Storage::PreparedFileFromFilesDialog(
|
||||
std::move(result),
|
||||
checkResult,
|
||||
showError,
|
||||
st::sendMediaPreviewSize,
|
||||
premium);
|
||||
|
||||
if (list) {
|
||||
chosen(std::move(*list));
|
||||
}
|
||||
};
|
||||
|
||||
const auto filters = (type == Ui::AlbumType::PhotoVideo)
|
||||
? FileDialog::PhotoVideoFilesFilter()
|
||||
: FileDialog::AllFilesFilter();
|
||||
FileDialog::GetOpenPath(
|
||||
controller->content().get(),
|
||||
tr::lng_choose_file(tr::now),
|
||||
filters,
|
||||
crl::guard(controller, callback));
|
||||
}
|
||||
|
||||
void EditPhotoImage(
|
||||
not_null<Window::SessionController*> controller,
|
||||
std::shared_ptr<Data::PhotoMedia> media,
|
||||
bool wasSpoiler,
|
||||
Fn<void(Ui::PreparedList)> done) {
|
||||
const auto large = media
|
||||
? media->image(Data::PhotoSize::Large)
|
||||
: nullptr;
|
||||
const auto parent = controller->content();
|
||||
const auto previewWidth = st::sendMediaPreviewSize;
|
||||
auto callback = [=](const Editor::PhotoModifications &mods) {
|
||||
if (!mods) {
|
||||
return;
|
||||
}
|
||||
const auto large = media->image(Data::PhotoSize::Large);
|
||||
if (!large) {
|
||||
return;
|
||||
}
|
||||
auto copy = large->original();
|
||||
auto list = Storage::PrepareMediaFromImage(
|
||||
std::move(copy),
|
||||
QByteArray(),
|
||||
previewWidth);
|
||||
|
||||
using ImageInfo = Ui::PreparedFileInformation::Image;
|
||||
auto &file = list.files.front();
|
||||
file.spoiler = wasSpoiler;
|
||||
const auto image = std::get_if<ImageInfo>(&file.information->media);
|
||||
|
||||
image->modifications = mods;
|
||||
const auto sideLimit = PhotoSideLimit();
|
||||
Storage::UpdateImageDetails(file, previewWidth, sideLimit);
|
||||
done(std::move(list));
|
||||
};
|
||||
const auto fileImage = std::make_shared<Image>(*large);
|
||||
auto editor = base::make_unique_q<Editor::PhotoEditor>(
|
||||
parent,
|
||||
&controller->window(),
|
||||
fileImage,
|
||||
Editor::PhotoModifications());
|
||||
const auto raw = editor.get();
|
||||
auto layer = std::make_unique<Editor::LayerWidget>(
|
||||
parent,
|
||||
std::move(editor));
|
||||
Editor::InitEditorLayer(layer.get(), raw, std::move(callback));
|
||||
controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditCaptionBox::EditCaptionBox(
|
||||
QWidget*,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<HistoryItem*> item)
|
||||
: EditCaptionBox({}, controller, item, PrepareEditText(item), {}, {}) {
|
||||
}
|
||||
|
||||
EditCaptionBox::EditCaptionBox(
|
||||
QWidget*,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<HistoryItem*> item,
|
||||
TextWithTags &&text,
|
||||
Ui::PreparedList &&list,
|
||||
Fn<void()> saved)
|
||||
: _controller(controller)
|
||||
, _historyItem(item)
|
||||
, _isAllowedEditMedia(item->media()
|
||||
@@ -135,7 +251,10 @@ EditCaptionBox::EditCaptionBox(
|
||||
tr::lng_photo_caption()))
|
||||
, _emojiToggle(base::make_unique_q<Ui::EmojiButton>(
|
||||
this,
|
||||
st::boxAttachEmoji)) {
|
||||
st::boxAttachEmoji))
|
||||
, _initialText(std::move(text))
|
||||
, _initialList(std::move(list))
|
||||
, _saved(std::move(saved)) {
|
||||
Expects(item->media() != nullptr);
|
||||
Expects(item->media()->allowsEditCaption());
|
||||
|
||||
@@ -148,6 +267,57 @@ EditCaptionBox::EditCaptionBox(
|
||||
|
||||
EditCaptionBox::~EditCaptionBox() = default;
|
||||
|
||||
void EditCaptionBox::StartMediaReplace(
|
||||
not_null<Window::SessionController*> controller,
|
||||
FullMsgId itemId,
|
||||
TextWithTags text,
|
||||
Fn<void()> saved) {
|
||||
const auto session = &controller->session();
|
||||
const auto item = session->data().message(itemId);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const auto show = [=](Ui::PreparedList &&list) mutable {
|
||||
controller->show(Box<EditCaptionBox>(
|
||||
controller,
|
||||
item,
|
||||
std::move(text),
|
||||
std::move(list),
|
||||
std::move(saved)));
|
||||
};
|
||||
ChooseReplacement(
|
||||
controller,
|
||||
ComputeAlbumType(item),
|
||||
crl::guard(controller, show));
|
||||
}
|
||||
|
||||
void EditCaptionBox::StartPhotoEdit(
|
||||
not_null<Window::SessionController*> controller,
|
||||
std::shared_ptr<Data::PhotoMedia> media,
|
||||
FullMsgId itemId,
|
||||
TextWithTags text,
|
||||
Fn<void()> saved) {
|
||||
const auto session = &controller->session();
|
||||
const auto item = session->data().message(itemId);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const auto hasSpoiler = item->media() && item->media()->hasSpoiler();
|
||||
EditPhotoImage(controller, media, hasSpoiler, [=](
|
||||
Ui::PreparedList &&list) mutable {
|
||||
const auto item = session->data().message(itemId);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
controller->show(Box<EditCaptionBox>(
|
||||
controller,
|
||||
item,
|
||||
std::move(text),
|
||||
std::move(list),
|
||||
std::move(saved)));
|
||||
});
|
||||
}
|
||||
|
||||
void EditCaptionBox::prepare() {
|
||||
addButton(tr::lng_settings_save(), [=] { save(); });
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
@@ -158,7 +328,9 @@ void EditCaptionBox::prepare() {
|
||||
setupEmojiPanel();
|
||||
setInitialText();
|
||||
|
||||
rebuildPreview();
|
||||
if (!setPreparedList(std::move(_initialList))) {
|
||||
rebuildPreview();
|
||||
}
|
||||
setupEditEventHandler();
|
||||
SetupShadowsToScrollContent(this, _scroll, _contentHeight.events());
|
||||
|
||||
@@ -290,16 +462,15 @@ void EditCaptionBox::setupField() {
|
||||
}
|
||||
|
||||
void EditCaptionBox::setInitialText() {
|
||||
const auto initial = PrepareEditText(_historyItem);
|
||||
_field->setTextWithTags(
|
||||
initial,
|
||||
_initialText,
|
||||
Ui::InputField::HistoryAction::Clear);
|
||||
auto cursor = _field->textCursor();
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
_field->setTextCursor(cursor);
|
||||
|
||||
_checkChangedTimer.setCallback([=] {
|
||||
if (_field->getTextWithAppliedMarkdown() == initial) {
|
||||
if (_field->getTextWithAppliedMarkdown() == _initialText) {
|
||||
setCloseByOutsideClick(true);
|
||||
}
|
||||
});
|
||||
@@ -353,132 +524,44 @@ void EditCaptionBox::setupControls() {
|
||||
}
|
||||
|
||||
void EditCaptionBox::setupEditEventHandler() {
|
||||
const auto toastParent = Ui::BoxShow(this).toastParent();
|
||||
const auto callback = [=](FileDialog::OpenResult &&result) {
|
||||
auto showError = [toastParent](tr::phrase<> t) {
|
||||
Ui::Toast::Show(toastParent, t(tr::now));
|
||||
};
|
||||
|
||||
const auto checkResult = [=](const Ui::PreparedList &list) {
|
||||
if (list.files.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
const auto &file = list.files.front();
|
||||
const auto mime = file.information->filemime;
|
||||
if (Core::IsMimeSticker(mime)) {
|
||||
showError(tr::lng_edit_media_invalid_file);
|
||||
return false;
|
||||
} else if (_albumType != Ui::AlbumType::None
|
||||
&& !file.canBeInAlbumType(_albumType)) {
|
||||
showError(tr::lng_edit_media_album_error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auto premium = _controller->session().premium();
|
||||
auto list = Storage::PreparedFileFromFilesDialog(
|
||||
std::move(result),
|
||||
checkResult,
|
||||
showError,
|
||||
st::sendMediaPreviewSize,
|
||||
premium);
|
||||
|
||||
if (list) {
|
||||
setPreparedList(std::move(*list));
|
||||
}
|
||||
};
|
||||
|
||||
const auto buttonCallback = [=] {
|
||||
const auto filters = (_albumType == Ui::AlbumType::PhotoVideo)
|
||||
? FileDialog::PhotoVideoFilesFilter()
|
||||
: FileDialog::AllFilesFilter();
|
||||
FileDialog::GetOpenPath(
|
||||
this,
|
||||
tr::lng_choose_file(tr::now),
|
||||
filters,
|
||||
crl::guard(this, callback));
|
||||
};
|
||||
|
||||
_editMediaClicks.events(
|
||||
) | rpl::start_with_next(
|
||||
buttonCallback,
|
||||
lifetime());
|
||||
) | rpl::start_with_next([=] {
|
||||
ChooseReplacement(_controller, _albumType, crl::guard(this, [=](
|
||||
Ui::PreparedList &&list) {
|
||||
setPreparedList(std::move(list));
|
||||
}));
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void EditCaptionBox::setupPhotoEditorEventHandler() {
|
||||
const auto openedOnce = lifetime().make_state<bool>(false);
|
||||
_photoEditorOpens.events(
|
||||
) | rpl::start_with_next([=, controller = _controller] {
|
||||
const auto increment = [=] {
|
||||
if (*openedOnce) {
|
||||
return;
|
||||
}
|
||||
if (_preparedList.files.empty()
|
||||
&& (!_photoMedia
|
||||
|| !_photoMedia->image(Data::PhotoSize::Large))) {
|
||||
return;
|
||||
} else if (!*openedOnce) {
|
||||
*openedOnce = true;
|
||||
controller->session().settings().incrementPhotoEditorHintShown();
|
||||
controller->session().saveSettings();
|
||||
};
|
||||
const auto clearError = [=] {
|
||||
}
|
||||
if (!_error.isEmpty()) {
|
||||
_error = QString();
|
||||
update();
|
||||
};
|
||||
const auto previewWidth = st::sendMediaPreviewSize;
|
||||
}
|
||||
if (!_preparedList.files.empty()) {
|
||||
increment();
|
||||
clearError();
|
||||
Editor::OpenWithPreparedFile(
|
||||
this,
|
||||
controller,
|
||||
&_preparedList.files.front(),
|
||||
previewWidth,
|
||||
st::sendMediaPreviewSize,
|
||||
[=] { rebuildPreview(); });
|
||||
} else if (_photoMedia) {
|
||||
const auto large = _photoMedia->image(Data::PhotoSize::Large);
|
||||
if (!large) {
|
||||
return;
|
||||
}
|
||||
increment();
|
||||
clearError();
|
||||
auto callback = [=](const Editor::PhotoModifications &mods) {
|
||||
if (!mods || !_photoMedia) {
|
||||
return;
|
||||
}
|
||||
const auto large = _photoMedia->image(Data::PhotoSize::Large);
|
||||
if (!large) {
|
||||
return;
|
||||
}
|
||||
auto copy = large->original();
|
||||
const auto wasSpoiler = hasSpoiler();
|
||||
|
||||
_preparedList = Storage::PrepareMediaFromImage(
|
||||
std::move(copy),
|
||||
QByteArray(),
|
||||
previewWidth);
|
||||
|
||||
using ImageInfo = Ui::PreparedFileInformation::Image;
|
||||
auto &file = _preparedList.files.front();
|
||||
file.spoiler = wasSpoiler;
|
||||
const auto image = std::get_if<ImageInfo>(
|
||||
&file.information->media);
|
||||
|
||||
image->modifications = mods;
|
||||
const auto sideLimit = PhotoSideLimit();
|
||||
Storage::UpdateImageDetails(file, previewWidth, sideLimit);
|
||||
rebuildPreview();
|
||||
};
|
||||
const auto fileImage = std::make_shared<Image>(*large);
|
||||
auto editor = base::make_unique_q<Editor::PhotoEditor>(
|
||||
this,
|
||||
&controller->window(),
|
||||
fileImage,
|
||||
Editor::PhotoModifications());
|
||||
const auto raw = editor.get();
|
||||
auto layer = std::make_unique<Editor::LayerWidget>(
|
||||
this,
|
||||
std::move(editor));
|
||||
Editor::InitEditorLayer(layer.get(), raw, std::move(callback));
|
||||
controller->showLayer(
|
||||
std::move(layer),
|
||||
Ui::LayerOption::KeepOther);
|
||||
} else {
|
||||
EditPhotoImage(_controller, _photoMedia, hasSpoiler(), [=](
|
||||
Ui::PreparedList &&list) {
|
||||
setPreparedList(std::move(list));
|
||||
});
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
@@ -780,13 +863,13 @@ void EditCaptionBox::save() {
|
||||
: SendMediaType::File,
|
||||
_field->getTextWithAppliedMarkdown(),
|
||||
action);
|
||||
closeBox();
|
||||
closeAfterSave();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto done = crl::guard(this, [=] {
|
||||
_saveRequestId = 0;
|
||||
closeBox();
|
||||
closeAfterSave();
|
||||
});
|
||||
|
||||
const auto fail = crl::guard(this, [=](const QString &error) {
|
||||
@@ -795,7 +878,7 @@ void EditCaptionBox::save() {
|
||||
_error = tr::lng_edit_error(tr::now);
|
||||
update();
|
||||
} else if (error == u"MESSAGE_NOT_MODIFIED"_q) {
|
||||
closeBox();
|
||||
closeAfterSave();
|
||||
} else if (error == u"MESSAGE_EMPTY"_q) {
|
||||
_field->setFocus();
|
||||
_field->showError();
|
||||
@@ -816,6 +899,16 @@ void EditCaptionBox::save() {
|
||||
_saveRequestId = Api::EditCaption(item, sending, options, done, fail);
|
||||
}
|
||||
|
||||
void EditCaptionBox::closeAfterSave() {
|
||||
const auto weak = MakeWeak(this);
|
||||
if (_saved) {
|
||||
_saved();
|
||||
}
|
||||
if (weak) {
|
||||
closeBox();
|
||||
}
|
||||
}
|
||||
|
||||
void EditCaptionBox::keyPressEvent(QKeyEvent *e) {
|
||||
const auto ctrl = e->modifiers().testFlag(Qt::ControlModifier);
|
||||
if ((e->key() == Qt::Key_E) && ctrl) {
|
||||
|
Reference in New Issue
Block a user