/* 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 "base/timer_rpl.h" #include "base/unixtime.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 "media/stories/media_stories_stealth.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 { namespace { [[nodiscard]] rpl::producer PlaceholderText( const std::shared_ptr &show) { return show->session().data().stories().stealthModeValue( ) | rpl::map([](Data::StealthMode value) { return value.enabledTill; }) | rpl::distinct_until_changed() | rpl::map([](TimeId till) { return rpl::single( rpl::empty ) | rpl::then( base::timer_each(250) ) | rpl::map([=] { return till - base::unixtime::now(); }) | rpl::take_while([](TimeId left) { return left > 0; }) | rpl::then( rpl::single(0) ) | rpl::map([](TimeId left) { return left ? tr::lng_stealth_mode_countdown( lt_left, rpl::single(TimeLeftText(left))) : tr::lng_story_reply_ph(); }) | rpl::flatten_latest(); }) | rpl::flatten_latest(); } } // namespace class ReplyArea::Cant final : public Ui::RpWidget { public: explicit Cant(not_null parent); private: void paintEvent(QPaintEvent *e) override; Ui::RoundRect _bg; }; ReplyArea::Cant::Cant(not_null 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) , _controls(std::make_unique( _controller->wrap(), HistoryView::ComposeControlsDescriptor{ .stOverride = &st::storiesComposeControls, .show = _controller->uiShow(), .unavailableEmojiPasted = [=](not_null emoji) { showPremiumToast(emoji); }, .mode = HistoryView::ComposeControlsMode::Normal, .sendMenuType = SendMenu::Type::SilentOnly, .stickerOrEmojiChosen = _controller->stickerOrEmojiChosen(), .customPlaceholder = PlaceholderText(_controller->uiShow()), .voiceCustomCancelText = tr::lng_record_cancel_stories(tr::now), .voiceLockFromBottom = true, .features = { .likes = true, .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)); finishSending(skipToast); _controls->clear(); } 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 document, Api::SendOptions options, std::optional 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 photo) { sendExistingPhoto(photo, {}); } bool ReplyArea::sendExistingPhoto( not_null 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 result, not_null 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 result, not_null bot, Api::SendOptions options, std::optional localMessageId) { auto action = prepareSendAction(options); action.generateLocal = true; session().api().sendInlineResult(bot, result, action, localMessageId); 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(); _controls->clear(); } void ReplyArea::finishSending(bool skipToast) { _controls->hidePanelsAnimated(); _controller->unfocusReply(); 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 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 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 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 data, std::optional 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(args)...); }; auto box = Box(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() : nullptr; session().api().sendFiles( std::move(group.list), type, base::take(caption), album, action); } finishSending(); } bool ReplyArea::confirmSendingFiles( QImage &&image, QByteArray &&content, std::optional 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 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->likeToggled( ) | rpl::start_with_next([=] { _controller->toggleLiked(); }, _lifetime); _controls->setMimeDataHook([=]( not_null 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, rpl::producer likedValue) { 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, .liked = std::move( likedValue ) | rpl::map([](const Data::ReactionId &id) { return !id.empty(); }), }); _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(_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(); } bool ReplyArea::focused() const { return _controls->focused(); } rpl::producer ReplyArea::focusedValue() const { return _controls->focusedValue(); } rpl::producer ReplyArea::hasSendTextValue() const { return _controls->hasSendTextValue(); } rpl::producer ReplyArea::activeValue() const { using namespace rpl::mappers; return rpl::combine( _controls->focusedValue(), _controls->recordingActiveValue(), _controls->tabbedPanelShownValue(), _controls->fieldMenuShownValue(), _choosingAttach.value(), _1 || _2 || _3 || _4 || _5 ) | rpl::distinct_until_changed(); } bool ReplyArea::ignoreWindowMove(QPoint position) const { return _controls->isRecordingPressed(); } void ReplyArea::tryProcessKeyInput(not_null e) { _controls->tryProcessKeyInput(e); } not_null ReplyArea::likeAnimationTarget() const { return _controls->likeAnimationTarget(); } void ReplyArea::showPremiumToast(not_null emoji) { // #TODO stories } } // namespace Media::Stories