683 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			683 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
This file is part of Telegram Desktop,
 | 
						|
the official desktop application for the Telegram messaging service.
 | 
						|
 | 
						|
For license and copyright information please follow this link:
 | 
						|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 | 
						|
*/
 | 
						|
#include "media/stories/media_stories_reply.h"
 | 
						|
 | 
						|
#include "api/api_common.h"
 | 
						|
#include "api/api_sending.h"
 | 
						|
#include "apiwrap.h"
 | 
						|
#include "base/call_delayed.h"
 | 
						|
#include "boxes/premium_limits_box.h"
 | 
						|
#include "boxes/send_files_box.h"
 | 
						|
#include "chat_helpers/compose/compose_show.h"
 | 
						|
#include "chat_helpers/tabbed_selector.h"
 | 
						|
#include "core/file_utilities.h"
 | 
						|
#include "core/mime_type.h"
 | 
						|
#include "data/stickers/data_custom_emoji.h"
 | 
						|
#include "data/data_document.h"
 | 
						|
#include "data/data_message_reaction_id.h"
 | 
						|
#include "data/data_session.h"
 | 
						|
#include "data/data_user.h"
 | 
						|
#include "history/view/controls/compose_controls_common.h"
 | 
						|
#include "history/view/controls/history_view_compose_controls.h"
 | 
						|
#include "history/history_item_helpers.h"
 | 
						|
#include "history/history.h"
 | 
						|
#include "inline_bots/inline_bot_result.h"
 | 
						|
#include "lang/lang_keys.h"
 | 
						|
#include "main/main_session.h"
 | 
						|
#include "media/stories/media_stories_controller.h"
 | 
						|
#include "menu/menu_send.h"
 | 
						|
#include "storage/localimageloader.h"
 | 
						|
#include "storage/storage_account.h"
 | 
						|
#include "storage/storage_media_prepare.h"
 | 
						|
#include "ui/chat/attach/attach_prepare.h"
 | 
						|
#include "ui/round_rect.h"
 | 
						|
#include "window/section_widget.h"
 | 
						|
#include "styles/style_boxes.h" // sendMediaPreviewSize.
 | 
						|
#include "styles/style_chat_helpers.h"
 | 
						|
#include "styles/style_media_view.h"
 | 
						|
 | 
						|
namespace Media::Stories {
 | 
						|
 | 
						|
class ReplyArea::Cant final : public Ui::RpWidget {
 | 
						|
public:
 | 
						|
	explicit Cant(not_null<QWidget*> parent);
 | 
						|
 | 
						|
private:
 | 
						|
	void paintEvent(QPaintEvent *e) override;
 | 
						|
 | 
						|
	Ui::RoundRect _bg;
 | 
						|
 | 
						|
};
 | 
						|
 | 
						|
ReplyArea::Cant::Cant(not_null<QWidget*> parent)
 | 
						|
: RpWidget(parent)
 | 
						|
, _bg(st::storiesRadius, st::storiesComposeBg) {
 | 
						|
	show();
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::Cant::paintEvent(QPaintEvent *e) {
 | 
						|
	auto p = QPainter(this);
 | 
						|
	_bg.paint(p, rect());
 | 
						|
 | 
						|
	p.setPen(st::storiesComposeGrayText);
 | 
						|
	p.setFont(st::normalFont);
 | 
						|
	p.drawText(
 | 
						|
		rect(),
 | 
						|
		tr::lng_stories_cant_reply(tr::now),
 | 
						|
		style::al_center);
 | 
						|
}
 | 
						|
 | 
						|
ReplyArea::ReplyArea(not_null<Controller*> controller)
 | 
						|
: _controller(controller)
 | 
						|
, _controls(std::make_unique<HistoryView::ComposeControls>(
 | 
						|
	_controller->wrap(),
 | 
						|
	HistoryView::ComposeControlsDescriptor{
 | 
						|
		.stOverride = &st::storiesComposeControls,
 | 
						|
		.show = _controller->uiShow(),
 | 
						|
		.unavailableEmojiPasted = [=](not_null<DocumentData*> emoji) {
 | 
						|
			showPremiumToast(emoji);
 | 
						|
		},
 | 
						|
		.mode = HistoryView::ComposeControlsMode::Normal,
 | 
						|
		.sendMenuType = SendMenu::Type::SilentOnly,
 | 
						|
		.stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(),
 | 
						|
		.voiceLockFromBottom = true,
 | 
						|
		.features = {
 | 
						|
			.sendAs = false,
 | 
						|
			.ttlInfo = false,
 | 
						|
			.botCommandSend = false,
 | 
						|
			.silentBroadcastToggle = false,
 | 
						|
			.attachBotsMenu = false,
 | 
						|
			.inlineBots = false,
 | 
						|
			.megagroupSet = false,
 | 
						|
			.stickersSettings = false,
 | 
						|
			.openStickerSets = false,
 | 
						|
			.autocompleteHashtags = false,
 | 
						|
			.autocompleteMentions = false,
 | 
						|
			.autocompleteCommands = false,
 | 
						|
		},
 | 
						|
	}
 | 
						|
)) {
 | 
						|
	initGeometry();
 | 
						|
	initActions();
 | 
						|
	_controls->hide();
 | 
						|
}
 | 
						|
 | 
						|
ReplyArea::~ReplyArea() {
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::initGeometry() {
 | 
						|
	rpl::combine(
 | 
						|
		_controller->layoutValue(),
 | 
						|
		_controls->height()
 | 
						|
	) | rpl::start_with_next([=](const Layout &layout, int height) {
 | 
						|
		const auto content = layout.content;
 | 
						|
		_controls->resizeToWidth(layout.controlsWidth);
 | 
						|
		if (_controls->heightCurrent() == height) {
 | 
						|
			const auto position = layout.controlsBottomPosition
 | 
						|
				- QPoint(0, height);
 | 
						|
			_controls->move(position.x(), position.y());
 | 
						|
			const auto &tabbed = st::storiesComposeControls.tabbed;
 | 
						|
			const auto upper = QRect(
 | 
						|
				position.x(),
 | 
						|
				content.y(),
 | 
						|
				layout.controlsWidth,
 | 
						|
				(position.y()
 | 
						|
					+ tabbed.autocompleteBottomSkip
 | 
						|
					- content.y()));
 | 
						|
			_controls->setAutocompleteBoundingRect(
 | 
						|
				layout.autocompleteRect.intersected(upper));
 | 
						|
		}
 | 
						|
	}, _lifetime);
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::sendReaction(const Data::ReactionId &id) {
 | 
						|
	Expects(_data.user != nullptr);
 | 
						|
 | 
						|
	auto message = Api::MessageToSend(prepareSendAction({}));
 | 
						|
	if (const auto emoji = id.emoji(); !emoji.isEmpty()) {
 | 
						|
		message.textWithTags = { emoji };
 | 
						|
	} else if (const auto customId = id.custom()) {
 | 
						|
		const auto document = _data.user->owner().document(customId);
 | 
						|
		if (const auto sticker = document->sticker()) {
 | 
						|
			const auto text = sticker->alt;
 | 
						|
			const auto id = Data::SerializeCustomEmojiId(customId);
 | 
						|
			message.textWithTags = {
 | 
						|
				text,
 | 
						|
				{ { 0, int(text.size()), Ui::InputField::CustomEmojiLink(id) } }
 | 
						|
			};
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (!message.textWithTags.empty()) {
 | 
						|
		send(std::move(message), {}, true);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::send(Api::SendOptions options) {
 | 
						|
	const auto webPageId = _controls->webPageId();
 | 
						|
 | 
						|
	auto message = Api::MessageToSend(prepareSendAction(options));
 | 
						|
	message.textWithTags = _controls->getTextWithAppliedMarkdown();
 | 
						|
	message.webPageId = webPageId;
 | 
						|
 | 
						|
	send(std::move(message), options);
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::send(
 | 
						|
		Api::MessageToSend message,
 | 
						|
		Api::SendOptions options,
 | 
						|
		bool skipToast) {
 | 
						|
	const auto error = GetErrorTextForSending(
 | 
						|
		_data.user,
 | 
						|
		{
 | 
						|
			.topicRootId = MsgId(0),
 | 
						|
			.text = &message.textWithTags,
 | 
						|
			.ignoreSlowmodeCountdown = (options.scheduled != 0),
 | 
						|
		});
 | 
						|
	if (!error.isEmpty()) {
 | 
						|
		_controller->uiShow()->showToast(error);
 | 
						|
	}
 | 
						|
 | 
						|
	session().api().sendMessage(std::move(message));
 | 
						|
 | 
						|
	_controls->clear();
 | 
						|
	finishSending(skipToast);
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::sendVoice(VoiceToSend &&data) {
 | 
						|
	auto action = prepareSendAction(data.options);
 | 
						|
	session().api().sendVoiceMessage(
 | 
						|
		data.bytes,
 | 
						|
		data.waveform,
 | 
						|
		data.duration,
 | 
						|
		std::move(action));
 | 
						|
 | 
						|
	_controls->clearListenState();
 | 
						|
	finishSending();
 | 
						|
}
 | 
						|
 | 
						|
bool ReplyArea::sendExistingDocument(
 | 
						|
		not_null<DocumentData*> document,
 | 
						|
		Api::SendOptions options,
 | 
						|
		std::optional<MsgId> localId) {
 | 
						|
	Expects(_data.user != nullptr);
 | 
						|
 | 
						|
	const auto show = _controller->uiShow();
 | 
						|
	const auto error = Data::RestrictionError(
 | 
						|
		_data.user,
 | 
						|
		ChatRestriction::SendStickers);
 | 
						|
	if (error) {
 | 
						|
		show->showToast(*error);
 | 
						|
		return false;
 | 
						|
	} else if (Window::ShowSendPremiumError(show, document)) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	Api::SendExistingDocument(
 | 
						|
		Api::MessageToSend(prepareSendAction(options)),
 | 
						|
		document,
 | 
						|
		localId);
 | 
						|
 | 
						|
	_controls->cancelReplyMessage();
 | 
						|
	finishSending();
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::sendExistingPhoto(not_null<PhotoData*> photo) {
 | 
						|
	sendExistingPhoto(photo, {});
 | 
						|
}
 | 
						|
 | 
						|
bool ReplyArea::sendExistingPhoto(
 | 
						|
		not_null<PhotoData*> photo,
 | 
						|
		Api::SendOptions options) {
 | 
						|
	Expects(_data.user != nullptr);
 | 
						|
 | 
						|
	const auto show = _controller->uiShow();
 | 
						|
	const auto error = Data::RestrictionError(
 | 
						|
		_data.user,
 | 
						|
		ChatRestriction::SendPhotos);
 | 
						|
	if (error) {
 | 
						|
		show->showToast(*error);
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	Api::SendExistingPhoto(
 | 
						|
		Api::MessageToSend(prepareSendAction(options)),
 | 
						|
		photo);
 | 
						|
 | 
						|
	_controls->cancelReplyMessage();
 | 
						|
	finishSending();
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::sendInlineResult(
 | 
						|
		not_null<InlineBots::Result*> result,
 | 
						|
		not_null<UserData*> bot) {
 | 
						|
	const auto errorText = result->getErrorOnSend(history());
 | 
						|
	if (!errorText.isEmpty()) {
 | 
						|
		_controller->uiShow()->showToast(errorText);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	sendInlineResult(result, bot, {}, std::nullopt);
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::sendInlineResult(
 | 
						|
		not_null<InlineBots::Result*> result,
 | 
						|
		not_null<UserData*> bot,
 | 
						|
		Api::SendOptions options,
 | 
						|
		std::optional<MsgId> localMessageId) {
 | 
						|
	auto action = prepareSendAction(options);
 | 
						|
	action.generateLocal = true;
 | 
						|
	session().api().sendInlineResult(bot, result, action, localMessageId);
 | 
						|
 | 
						|
	_controls->clear();
 | 
						|
 | 
						|
	auto &bots = cRefRecentInlineBots();
 | 
						|
	const auto index = bots.indexOf(bot);
 | 
						|
	if (index) {
 | 
						|
		if (index > 0) {
 | 
						|
			bots.removeAt(index);
 | 
						|
		} else if (bots.size() >= RecentInlineBotsLimit) {
 | 
						|
			bots.resize(RecentInlineBotsLimit - 1);
 | 
						|
		}
 | 
						|
		bots.push_front(bot);
 | 
						|
		bot->session().local().writeRecentHashtagsAndBots();
 | 
						|
	}
 | 
						|
	finishSending();
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::finishSending(bool skipToast) {
 | 
						|
	_controls->hidePanelsAnimated();
 | 
						|
	_controller->wrap()->setFocus();
 | 
						|
	if (!skipToast) {
 | 
						|
		_controller->uiShow()->showToast(
 | 
						|
			tr::lng_stories_reply_sent(tr::now));
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::uploadFile(
 | 
						|
		const QByteArray &fileContent,
 | 
						|
		SendMediaType type) {
 | 
						|
	session().api().sendFile(fileContent, type, prepareSendAction({}));
 | 
						|
}
 | 
						|
 | 
						|
bool ReplyArea::showSendingFilesError(
 | 
						|
		const Ui::PreparedList &list) const {
 | 
						|
	return showSendingFilesError(list, std::nullopt);
 | 
						|
}
 | 
						|
 | 
						|
bool ReplyArea::showSendingFilesError(
 | 
						|
		const Ui::PreparedList &list,
 | 
						|
		std::optional<bool> compress) const {
 | 
						|
	const auto text = [&] {
 | 
						|
		const auto peer = _data.user;
 | 
						|
		const auto error = Data::FileRestrictionError(peer, list, compress);
 | 
						|
		if (error) {
 | 
						|
			return *error;
 | 
						|
		}
 | 
						|
		using Error = Ui::PreparedList::Error;
 | 
						|
		switch (list.error) {
 | 
						|
		case Error::None: return QString();
 | 
						|
		case Error::EmptyFile:
 | 
						|
		case Error::Directory:
 | 
						|
		case Error::NonLocalUrl: return tr::lng_send_image_empty(
 | 
						|
			tr::now,
 | 
						|
			lt_name,
 | 
						|
			list.errorData);
 | 
						|
		case Error::TooLargeFile: return u"(toolarge)"_q;
 | 
						|
		}
 | 
						|
		return tr::lng_forward_send_files_cant(tr::now);
 | 
						|
	}();
 | 
						|
	if (text.isEmpty()) {
 | 
						|
		return false;
 | 
						|
	} else if (text == u"(toolarge)"_q) {
 | 
						|
		const auto fileSize = list.files.back().size;
 | 
						|
		_controller->uiShow()->showBox(Box(
 | 
						|
			FileSizeLimitBox,
 | 
						|
			&session(),
 | 
						|
			fileSize,
 | 
						|
			&st::storiesComposePremium));
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
 | 
						|
	_controller->uiShow()->showToast(text);
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
not_null<History*> ReplyArea::history() const {
 | 
						|
	Expects(_data.user != nullptr);
 | 
						|
 | 
						|
	return _data.user->owner().history(_data.user);
 | 
						|
}
 | 
						|
 | 
						|
Api::SendAction ReplyArea::prepareSendAction(
 | 
						|
		Api::SendOptions options) const {
 | 
						|
	Expects(_data.user != nullptr);
 | 
						|
 | 
						|
	auto result = Api::SendAction(history(), options);
 | 
						|
	result.options.sendAs = _controls->sendAsPeer();
 | 
						|
	result.replyTo.storyId = { .peer = _data.user->id, .story = _data.id };
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::chooseAttach(
 | 
						|
		std::optional<bool> overrideSendImagesAsPhotos) {
 | 
						|
	_chooseAttachRequest = false;
 | 
						|
	if (!_data.user) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto user = not_null(_data.user);
 | 
						|
	if (const auto error = Data::AnyFileRestrictionError(user)) {
 | 
						|
		_controller->uiShow()->showToast(*error);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	const auto filter = (overrideSendImagesAsPhotos == true)
 | 
						|
		? FileDialog::ImagesOrAllFilter()
 | 
						|
		: FileDialog::AllOrImagesFilter();
 | 
						|
	const auto weak = make_weak(&_shownUserGuard);
 | 
						|
	const auto callback = [=](FileDialog::OpenResult &&result) {
 | 
						|
		const auto guard = gsl::finally([&] {
 | 
						|
			_choosingAttach = false;
 | 
						|
		});
 | 
						|
		if (!weak
 | 
						|
			|| (result.paths.isEmpty() && result.remoteContent.isEmpty())) {
 | 
						|
			return;
 | 
						|
		} else if (!result.remoteContent.isEmpty()) {
 | 
						|
			auto read = Images::Read({
 | 
						|
				.content = result.remoteContent,
 | 
						|
			});
 | 
						|
			if (!read.image.isNull() && !read.animated) {
 | 
						|
				confirmSendingFiles(
 | 
						|
					std::move(read.image),
 | 
						|
					std::move(result.remoteContent),
 | 
						|
					overrideSendImagesAsPhotos);
 | 
						|
			} else {
 | 
						|
				uploadFile(result.remoteContent, SendMediaType::File);
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			const auto premium = session().premium();
 | 
						|
			auto list = Storage::PrepareMediaList(
 | 
						|
				result.paths,
 | 
						|
				st::sendMediaPreviewSize,
 | 
						|
				premium);
 | 
						|
			list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;
 | 
						|
			confirmSendingFiles(std::move(list));
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	_choosingAttach = true;
 | 
						|
	FileDialog::GetOpenPaths(
 | 
						|
		_controller->wrap().get(),
 | 
						|
		tr::lng_choose_files(tr::now),
 | 
						|
		filter,
 | 
						|
		crl::guard(this, callback),
 | 
						|
		crl::guard(this, [=] { _choosingAttach = false; }));
 | 
						|
}
 | 
						|
 | 
						|
bool ReplyArea::confirmSendingFiles(
 | 
						|
		not_null<const QMimeData*> data,
 | 
						|
		std::optional<bool> overrideSendImagesAsPhotos,
 | 
						|
		const QString &insertTextOnCancel) {
 | 
						|
	const auto hasImage = data->hasImage();
 | 
						|
	const auto premium = session().user()->isPremium();
 | 
						|
 | 
						|
	if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) {
 | 
						|
		auto list = Storage::PrepareMediaList(
 | 
						|
			urls,
 | 
						|
			st::sendMediaPreviewSize,
 | 
						|
			premium);
 | 
						|
		if (list.error != Ui::PreparedList::Error::NonLocalUrl) {
 | 
						|
			if (list.error == Ui::PreparedList::Error::None
 | 
						|
				|| !hasImage) {
 | 
						|
				const auto emptyTextOnCancel = QString();
 | 
						|
				list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;
 | 
						|
				confirmSendingFiles(std::move(list), emptyTextOnCancel);
 | 
						|
				return true;
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if (auto read = Core::ReadMimeImage(data)) {
 | 
						|
		confirmSendingFiles(
 | 
						|
			std::move(read.image),
 | 
						|
			std::move(read.content),
 | 
						|
			overrideSendImagesAsPhotos,
 | 
						|
			insertTextOnCancel);
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
	return false;
 | 
						|
}
 | 
						|
 | 
						|
bool ReplyArea::confirmSendingFiles(
 | 
						|
		Ui::PreparedList &&list,
 | 
						|
		const QString &insertTextOnCancel) {
 | 
						|
	if (_controls->confirmMediaEdit(list)) {
 | 
						|
		return true;
 | 
						|
	} else if (showSendingFilesError(list)) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	const auto show = _controller->uiShow();
 | 
						|
	auto confirmed = [=](auto &&...args) {
 | 
						|
		sendingFilesConfirmed(std::forward<decltype(args)>(args)...);
 | 
						|
	};
 | 
						|
	auto box = Box<SendFilesBox>(SendFilesBoxDescriptor{
 | 
						|
		.show = show,
 | 
						|
		.list = std::move(list),
 | 
						|
		.caption = _controls->getTextWithAppliedMarkdown(),
 | 
						|
		.limits = DefaultLimitsForPeer(_data.user),
 | 
						|
		.check = DefaultCheckForPeer(show, _data.user),
 | 
						|
		.sendType = Api::SendType::Normal,
 | 
						|
		.sendMenuType = SendMenu::Type::SilentOnly,
 | 
						|
		.stOverride = &st::storiesComposeControls,
 | 
						|
		.confirmed = crl::guard(this, confirmed),
 | 
						|
		.cancelled = _controls->restoreTextCallback(insertTextOnCancel),
 | 
						|
	});
 | 
						|
	if (const auto shown = show->show(std::move(box))) {
 | 
						|
		shown->setCloseByOutsideClick(false);
 | 
						|
	}
 | 
						|
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::sendingFilesConfirmed(
 | 
						|
		Ui::PreparedList &&list,
 | 
						|
		Ui::SendFilesWay way,
 | 
						|
		TextWithTags &&caption,
 | 
						|
		Api::SendOptions options,
 | 
						|
		bool ctrlShiftEnter) {
 | 
						|
	Expects(list.filesToProcess.empty());
 | 
						|
 | 
						|
	if (showSendingFilesError(list, way.sendImagesAsPhotos())) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	auto groups = DivideByGroups(
 | 
						|
		std::move(list),
 | 
						|
		way,
 | 
						|
		_data.user->slowmodeApplied());
 | 
						|
	const auto type = way.sendImagesAsPhotos()
 | 
						|
		? SendMediaType::Photo
 | 
						|
		: SendMediaType::File;
 | 
						|
	auto action = prepareSendAction(options);
 | 
						|
	action.clearDraft = false;
 | 
						|
	if ((groups.size() != 1 || !groups.front().sentWithCaption())
 | 
						|
		&& !caption.text.isEmpty()) {
 | 
						|
		auto message = Api::MessageToSend(action);
 | 
						|
		message.textWithTags = base::take(caption);
 | 
						|
		session().api().sendMessage(std::move(message));
 | 
						|
	}
 | 
						|
	for (auto &group : groups) {
 | 
						|
		const auto album = (group.type != Ui::AlbumType::None)
 | 
						|
			? std::make_shared<SendingAlbum>()
 | 
						|
			: nullptr;
 | 
						|
		session().api().sendFiles(
 | 
						|
			std::move(group.list),
 | 
						|
			type,
 | 
						|
			base::take(caption),
 | 
						|
			album,
 | 
						|
			action);
 | 
						|
	}
 | 
						|
	finishSending();
 | 
						|
}
 | 
						|
 | 
						|
bool ReplyArea::confirmSendingFiles(
 | 
						|
		QImage &&image,
 | 
						|
		QByteArray &&content,
 | 
						|
		std::optional<bool> overrideSendImagesAsPhotos,
 | 
						|
		const QString &insertTextOnCancel) {
 | 
						|
	if (image.isNull()) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	auto list = Storage::PrepareMediaFromImage(
 | 
						|
		std::move(image),
 | 
						|
		std::move(content),
 | 
						|
		st::sendMediaPreviewSize);
 | 
						|
	list.overrideSendImagesAsPhotos = overrideSendImagesAsPhotos;
 | 
						|
	return confirmSendingFiles(std::move(list), insertTextOnCancel);
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::initActions() {
 | 
						|
	_controls->cancelRequests(
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
		_controller->unfocusReply();
 | 
						|
	}, _lifetime);
 | 
						|
 | 
						|
	_controls->sendRequests(
 | 
						|
	) | rpl::start_with_next([=](Api::SendOptions options) {
 | 
						|
		send(options);
 | 
						|
	}, _lifetime);
 | 
						|
 | 
						|
	_controls->sendVoiceRequests(
 | 
						|
	) | rpl::start_with_next([=](VoiceToSend &&data) {
 | 
						|
		sendVoice(std::move(data));
 | 
						|
	}, _lifetime);
 | 
						|
 | 
						|
	_controls->attachRequests(
 | 
						|
	) | rpl::filter([=] {
 | 
						|
		return !_chooseAttachRequest;
 | 
						|
	}) | rpl::start_with_next([=](std::optional<bool> overrideCompress) {
 | 
						|
		_chooseAttachRequest = true;
 | 
						|
		base::call_delayed(
 | 
						|
			st::storiesAttach.ripple.hideDuration,
 | 
						|
			this,
 | 
						|
			[=] { chooseAttach(overrideCompress); });
 | 
						|
	}, _lifetime);
 | 
						|
 | 
						|
	_controls->fileChosen(
 | 
						|
	) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
 | 
						|
		_controller->uiShow()->hideLayer();
 | 
						|
		const auto localId = data.messageSendingFrom.localId;
 | 
						|
		sendExistingDocument(data.document, data.options, localId);
 | 
						|
	}, _lifetime);
 | 
						|
 | 
						|
	_controls->photoChosen(
 | 
						|
	) | rpl::start_with_next([=](ChatHelpers::PhotoChosen chosen) {
 | 
						|
		sendExistingPhoto(chosen.photo, chosen.options);
 | 
						|
	}, _lifetime);
 | 
						|
 | 
						|
	_controls->inlineResultChosen(
 | 
						|
	) | rpl::start_with_next([=](ChatHelpers::InlineChosen chosen) {
 | 
						|
		const auto localId = chosen.messageSendingFrom.localId;
 | 
						|
		sendInlineResult(chosen.result, chosen.bot, chosen.options, localId);
 | 
						|
	}, _lifetime);
 | 
						|
 | 
						|
	_controls->setMimeDataHook([=](
 | 
						|
			not_null<const QMimeData*> data,
 | 
						|
			Ui::InputField::MimeAction action) {
 | 
						|
		if (action == Ui::InputField::MimeAction::Check) {
 | 
						|
			return Core::CanSendFiles(data);
 | 
						|
		} else if (action == Ui::InputField::MimeAction::Insert) {
 | 
						|
			return confirmSendingFiles(
 | 
						|
				data,
 | 
						|
				std::nullopt,
 | 
						|
				Core::ReadMimeText(data));
 | 
						|
		}
 | 
						|
		Unexpected("action in MimeData hook.");
 | 
						|
	});
 | 
						|
 | 
						|
	_controls->lockShowStarts(
 | 
						|
	) | rpl::start_with_next([=] {
 | 
						|
	}, _lifetime);
 | 
						|
 | 
						|
	_controls->show();
 | 
						|
	_controls->finishAnimating();
 | 
						|
	_controls->showFinished();
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::show(ReplyAreaData data) {
 | 
						|
	if (_data == data) {
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	const auto userChanged = (_data.user != data.user);
 | 
						|
	_data = data;
 | 
						|
	if (!userChanged) {
 | 
						|
		if (_data.user) {
 | 
						|
			_controls->clear();
 | 
						|
		}
 | 
						|
		return;
 | 
						|
	}
 | 
						|
	invalidate_weak_ptrs(&_shownUserGuard);
 | 
						|
	const auto user = data.user;
 | 
						|
	const auto history = user ? user->owner().history(user).get() : nullptr;
 | 
						|
	_controls->setHistory({
 | 
						|
		.history = history,
 | 
						|
	});
 | 
						|
	_controls->clear();
 | 
						|
	const auto hidden = user && user->isSelf();
 | 
						|
	const auto cant = !user || user->isServiceUser();
 | 
						|
	if (!hidden && !cant) {
 | 
						|
		_controls->show();
 | 
						|
	} else {
 | 
						|
		_controls->hide();
 | 
						|
		if (cant) {
 | 
						|
			_cant = std::make_unique<Cant>(_controller->wrap());
 | 
						|
			_controller->layoutValue(
 | 
						|
			) | rpl::start_with_next([=](const Layout &layout) {
 | 
						|
				const auto height = st::storiesComposeControls.attach.height;
 | 
						|
				const auto position = layout.controlsBottomPosition
 | 
						|
					- QPoint(0, height);
 | 
						|
				_cant->setGeometry(
 | 
						|
					{ position, QSize{ layout.controlsWidth, height } });
 | 
						|
			}, _cant->lifetime());
 | 
						|
		} else {
 | 
						|
			_cant = nullptr;
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
Main::Session &ReplyArea::session() const {
 | 
						|
	Expects(_data.user != nullptr);
 | 
						|
 | 
						|
	return _data.user->session();
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<bool> ReplyArea::focusedValue() const {
 | 
						|
	return _controls->focusedValue();
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<bool> ReplyArea::hasSendTextValue() const {
 | 
						|
	return _controls->hasSendTextValue();
 | 
						|
}
 | 
						|
 | 
						|
rpl::producer<bool> ReplyArea::activeValue() const {
 | 
						|
	using namespace rpl::mappers;
 | 
						|
	return rpl::combine(
 | 
						|
		_controls->focusedValue(),
 | 
						|
		_controls->recordingValue(),
 | 
						|
		_controls->tabbedPanelShownValue(),
 | 
						|
		_choosingAttach.value(),
 | 
						|
		_1 || _2 || _3 || _4
 | 
						|
	) | rpl::distinct_until_changed();
 | 
						|
}
 | 
						|
 | 
						|
void ReplyArea::showPremiumToast(not_null<DocumentData*> emoji) {
 | 
						|
	// #TODO stories
 | 
						|
}
 | 
						|
 | 
						|
} // namespace Media::Stories
 |