/* 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 "iv/iv_instance.h" #include "apiwrap.h" #include "base/platform/base_platform_info.h" #include "base/qt_signal_producer.h" #include "base/unixtime.h" #include "boxes/share_box.h" #include "core/application.h" #include "core/file_utilities.h" #include "core/shortcuts.h" #include "core/click_handler_types.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_cloud_file.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_file_origin.h" #include "data/data_location.h" #include "data/data_media_types.h" #include "data/data_photo_media.h" #include "data/data_session.h" #include "data/data_thread.h" #include "data/data_web_page.h" #include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/view/history_view_element.h" #include "history/view/media/history_view_location.h" #include "history/view/media/history_view_photo.h" #include "info/profile/info_profile_values.h" #include "iv/markdown/iv_markdown_controller.h" #include "iv/markdown/iv_markdown_history_view_media.h" #include "iv/iv_controller.h" #include "iv/iv_data.h" #include "iv/iv_prepare.h" #include "lang/lang_keys.h" #include "lottie/lottie_common.h" // Lottie::ReadContent. #include "main/main_account.h" #include "main/main_session.h" #include "main/session/session_show.h" #include "media/streaming/media_streaming_loader.h" #include "media/view/media_view_open_common.h" #include "storage/file_download.h" #include "storage/storage_account.h" #include "ui/boxes/confirm_box.h" #include "ui/layers/layer_widget.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/basic_click_handlers.h" #include "ui/dynamic_image.h" #include "ui/dynamic_thumbnails.h" #include "ui/painter.h" #include "webview/webview_data_stream_memory.h" #include "webview/webview_interface.h" #include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_session_controller_link_info.h" #include "styles/palette.h" #include "styles/style_chat.h" #include #include #include #include #include namespace Iv { struct NativeIvChannelContext { uint64 channelId = 0; QString username; }; [[nodiscard]] NativeIvChannelContext ParseNativeIvChannelContext( const QString &context) { const auto separator = context.indexOf(u'\n'); return { .channelId = (separator >= 0) ? context.mid(0, separator).toULongLong() : context.toULongLong(), .username = (separator >= 0) ? context.mid(separator + 1) : QString(), }; } [[nodiscard]] QString SerializeNativeIvChannelContext( uint64 channelId, QString username) { auto result = QString::number(channelId); if (!username.isEmpty()) { result += u"\n"_q + username; } return result; } [[nodiscard]] QString ResolveNativeIvChannelUsername( const QString &channelUsername, const QString &contextUsername) { return !channelUsername.isEmpty() ? channelUsername : contextUsername; } namespace { constexpr auto kGeoPointScale = 1; constexpr auto kGeoPointZoomMin = 13; constexpr auto kMaxLoadParts = 5; constexpr auto kKeepLoadingParts = 8; constexpr auto kAllowPageReloadAfter = 3 * crl::time(1000); enum class CachedPagePhotoImageKind { Thumbnail, Full, }; [[nodiscard]] Window::SessionController *CurrentSessionController( not_null session) { if (const auto window = Core::App().activeWindow()) { if (const auto current = window->sessionController(); current && (¤t->session() == session)) { return current; } } return nullptr; } class CachedPagePhotoDynamicImage final : public Ui::DynamicImage { public: CachedPagePhotoDynamicImage( std::shared_ptr<::Data::PhotoMedia> media, not_null photo, ::Data::FileOrigin origin, CachedPagePhotoImageKind kind) : _media(std::move(media)) , _photo(photo) , _origin(std::move(origin)) , _kind(kind) { } [[nodiscard]] std::shared_ptr clone() override { return std::make_shared( _media, _photo, _origin, _kind); } [[nodiscard]] QImage image(int size) override { Q_UNUSED(size); ensureWanted(); if (const auto image = resolvedImage()) { return image->original(); } return QImage(); } void subscribeToUpdates(Fn callback) override { if (!callback) { _subscription = {}; return; } _subscription = _photo->owner().photoLoadProgress( ) | rpl::filter([photo = _photo](not_null updated) { return (updated == photo); }) | rpl::on_next([callback = std::move(callback)] { callback(); }); } private: void ensureWanted() { switch (_kind) { case CachedPagePhotoImageKind::Thumbnail: _media->wanted(::Data::PhotoSize::Small, _origin); break; case CachedPagePhotoImageKind::Full: _media->wanted(::Data::PhotoSize::Large, _origin); break; } } [[nodiscard]] Image *resolvedImage() const { switch (_kind) { case CachedPagePhotoImageKind::Full: if (const auto large = _media->image(::Data::PhotoSize::Large)) { return large; } [[fallthrough]]; case CachedPagePhotoImageKind::Thumbnail: if (const auto small = _media->image(::Data::PhotoSize::Small)) { return small; } else if (const auto thumbnail = _media->image(::Data::PhotoSize::Thumbnail)) { return thumbnail; } return _media->thumbnailInline(); } return nullptr; } const std::shared_ptr<::Data::PhotoMedia> _media; const not_null _photo; const ::Data::FileOrigin _origin; const CachedPagePhotoImageKind _kind; rpl::lifetime _subscription; }; class CachedPagePhotoRuntime final : public Markdown::PhotoRuntime { public: CachedPagePhotoRuntime( not_null session, not_null photo, ::Data::FileOrigin origin) : _session(session) , _photo(photo) , _origin(std::move(origin)) , _media(photo->createMediaView()) { } [[nodiscard]] std::shared_ptr thumbnail( QSize size) const override { Q_UNUSED(size); _media->wanted(::Data::PhotoSize::Small, _origin); return std::make_shared( _media, _photo, _origin, CachedPagePhotoImageKind::Thumbnail); } [[nodiscard]] std::shared_ptr full( QSize size) const override { Q_UNUSED(size); _media->wanted(::Data::PhotoSize::Large, _origin); return std::make_shared( _media, _photo, _origin, CachedPagePhotoImageKind::Full); } [[nodiscard]] bool loaded() const override { _media->wanted(::Data::PhotoSize::Large, _origin); return _media->loaded(); } [[nodiscard]] bool loading() const override { _media->wanted(::Data::PhotoSize::Large, _origin); return _photo->displayLoading(); } [[nodiscard]] double progress() const override { _media->wanted(::Data::PhotoSize::Large, _origin); return _media->progress(); } void open(Qt::MouseButton button) const override { if (button != Qt::LeftButton && button != Qt::MiddleButton) { return; } if (const auto window = Core::App().activeWindow()) { const auto item = (HistoryItem*)nullptr; window->openInMediaView({ CurrentSessionController(_session), _photo, item, MsgId(0), PeerId(0), }); } } private: const not_null _session; const not_null _photo; const ::Data::FileOrigin _origin; const std::shared_ptr<::Data::PhotoMedia> _media; }; [[nodiscard]] ImageWithLocation CachedPageMapImageData( double latitude, double longitude, uint64 accessHash, QSize size, int zoom) { const auto location = GeoPointLocation{ .lat = latitude, .lon = longitude, .access = accessHash, .width = std::max(size.width(), 1), .height = std::max(size.height(), 1), .zoom = std::max(zoom, kGeoPointZoomMin), .scale = kGeoPointScale, }; return { .location = ImageLocation( { location }, location.width, location.height), }; } [[nodiscard]] ::Data::LocationPoint CachedPageMapPoint( double latitude, double longitude, uint64 accessHash) { const auto point = MTP_geoPoint( MTP_flags(0), MTP_double(longitude), MTP_double(latitude), MTP_long(accessHash), MTP_int(0)); return ::Data::LocationPoint(point.c_geoPoint()); } [[nodiscard]] not_null AddIvNeutralHostMessage( not_null history, QString pageUrl, DocumentData *document = nullptr, PhotoData *photo = nullptr) { const auto item = history->addNewLocalMessage({ .id = history->nextNonHistoryEntryId(), .flags = (MessageFlag::FakeHistoryItem | MessageFlag::Local | MessageFlag::HideDisplayDate), .date = base::unixtime::now(), }, TextWithEntities(), MTP_messageMediaEmpty()); item->setMediaForInstantView(std::move(pageUrl), document, photo); return item; } [[nodiscard]] bool CanHostNativeIvVideoDocument( not_null document) { return !document->isVideoMessage() && (document->isVideoFile() || document->isAnimation()); } [[nodiscard]] ::Data::MediaFile::Args CachedPageVideoMediaArgs( not_null session, not_null document) { const auto video = document->video(); return { .hasQualitiesList = video && !video->qualities.empty(), .skipPremiumEffect = !session->premium(), }; } class CachedPageDocumentRuntime final : public Markdown::DocumentRuntime { public: CachedPageDocumentRuntime( not_null session, not_null document, ::Data::FileOrigin origin) : _session(session) , _document(document) , _origin(std::move(origin)) , _media(document->createMediaView()) { } [[nodiscard]] std::shared_ptr thumbnail( QSize size) const override { Q_UNUSED(size); return Ui::MakeDocumentThumbnailFit(_document, _origin); } [[nodiscard]] std::shared_ptr full( QSize size) const override { Q_UNUSED(size); return Ui::MakeDocumentThumbnail(_document, _origin); } [[nodiscard]] bool loaded() const override { return _media->loaded(); } [[nodiscard]] bool loading() const override { return _document->displayLoading(); } [[nodiscard]] double progress() const override { return _document->progress(); } void open(Qt::MouseButton button) const override { if (button != Qt::LeftButton && button != Qt::MiddleButton) { return; } if (const auto window = Core::App().activeWindow()) { const auto item = (HistoryItem*)nullptr; window->openInMediaView({ CurrentSessionController(_session), _document, item, MsgId(0), PeerId(0), }); } } private: const not_null _session; const not_null _document; const ::Data::FileOrigin _origin; const std::shared_ptr<::Data::DocumentMedia> _media; }; class CachedPageInlineDocumentImage final : public Ui::DynamicImage { public: CachedPageInlineDocumentImage( not_null document, ::Data::FileOrigin origin) : _document(document) , _origin(std::move(origin)) , _media(document->createMediaView()) { } [[nodiscard]] std::shared_ptr clone() override { return std::make_shared( _document, _origin); } [[nodiscard]] QImage image(int size) override { Q_UNUSED(size); ensureWanted(); if (const auto image = resolvedImage()) { return image->original(); } return QImage(); } void subscribeToUpdates(Fn callback) override { _subscription.destroy(); if (!callback) { return; } ensureWanted(); if (_media->thumbnail()) { return; } _document->session().downloaderTaskFinished( ) | rpl::filter([=] { return (_media->thumbnail() != nullptr); }) | rpl::take(1) | rpl::on_next(std::move(callback), _subscription); } private: void ensureWanted() { _media->thumbnailWanted(_origin); } [[nodiscard]] Image *resolvedImage() const { if (const auto image = _media->thumbnail()) { return image; } return _media->thumbnailInline(); } const not_null _document; const ::Data::FileOrigin _origin; const std::shared_ptr<::Data::DocumentMedia> _media; rpl::lifetime _subscription; }; class CachedPageMapDynamicImage final : public Ui::DynamicImage { public: CachedPageMapDynamicImage( not_null<::Data::CloudImage*> data, not_null session, ::Data::FileOrigin origin) : _data(data) , _session(session) , _origin(std::move(origin)) { } [[nodiscard]] std::shared_ptr clone() override { return std::make_shared( _data, _session, _origin); } [[nodiscard]] QImage image(int size) override { Q_UNUSED(size); const auto loaded = _view ? *_view : QImage(); if (loaded.isNull()) { return QImage(); } const auto paletteVersion = style::PaletteVersion(); if (_prepared.size() == loaded.size() && _prepared.devicePixelRatio() == loaded.devicePixelRatio() && _paletteVersion == paletteVersion) { return _prepared; } _paletteVersion = paletteVersion; _prepared = loaded.copy(); _prepared.setDevicePixelRatio(loaded.devicePixelRatio()); const auto ratio = loaded.devicePixelRatio(); const auto width = int(loaded.width() / ratio); const auto height = int(loaded.height() / ratio); const auto markerSize = std::min(width, height); auto p = Painter(&_prepared); auto hq = PainterHighQualityEnabler(p); const auto pinScale = std::min({ 1.0, width / (st::historyMapPoint.height() * 2.5), height / (st::historyMapPoint.height() * 2.5), }); const auto center = QPointF(width / 2.0, height / 2.0); p.translate(center); p.scale(pinScale, pinScale); p.translate(-center); const auto paintMarker = [&](const style::icon &icon) { icon.paint( p, (width - icon.width()) / 2, (height / 2) - icon.height(), markerSize); }; paintMarker(st::historyMapPoint); paintMarker(st::historyMapPointInner); return _prepared; } void subscribeToUpdates(Fn callback) override { _subscription.destroy(); if (!callback) { _view = nullptr; _prepared = QImage(); return; } _view = _data->createView(); _data->load(_session, _origin); if (!_view->isNull()) { return; } _subscription = _session->downloaderTaskFinished( ) | rpl::filter([=] { return !_view->isNull(); }) | rpl::take(1) | rpl::on_next([=] { _prepared = QImage(); callback(); }); } private: const not_null<::Data::CloudImage*> _data; const not_null _session; const ::Data::FileOrigin _origin; std::shared_ptr _view; QImage _prepared; int _paletteVersion = 0; rpl::lifetime _subscription; }; class CachedPageMapRuntime final : public Markdown::MapRuntime { public: CachedPageMapRuntime( not_null session, ::Data::FileOrigin origin, double latitude, double longitude, uint64 accessHash, QSize size, int zoom) : _session(session) , _origin(std::move(origin)) , _image(session, CachedPageMapImageData( latitude, longitude, accessHash, size, zoom)) { } [[nodiscard]] std::shared_ptr thumbnail( QSize size) const override { Q_UNUSED(size); ensureLoaded(); return std::make_shared( &_image, _session, _origin); } [[nodiscard]] std::shared_ptr full( QSize size) const override { Q_UNUSED(size); ensureLoaded(); return std::make_shared( &_image, _session, _origin); } [[nodiscard]] bool loaded() const override { ensureLoaded(); return _image.loadedOnce(); } [[nodiscard]] bool loading() const override { ensureLoaded(); return _image.loading(); } [[nodiscard]] double progress() const override { ensureLoaded(); return _image.loadedOnce() ? 1. : 0.; } private: void ensureLoaded() const { _image.load(_session, _origin); } const not_null _session; const ::Data::FileOrigin _origin; mutable ::Data::CloudImage _image; }; class CachedPageChannelRuntime final : public Markdown::ChannelRuntime { public: CachedPageChannelRuntime( not_null channel, QString context, Fn openChannel, Fn joinChannel) : _channel(channel) , _context(std::move(context)) , _openChannel(std::move(openChannel)) , _joinChannel(std::move(joinChannel)) { } [[nodiscard]] bool joinVisible() const override { return !_channel->amIn(); } void open(Qt::MouseButton button) const override { if ((button == Qt::LeftButton || button == Qt::MiddleButton) && _openChannel) { _openChannel(_context); } } void join(Qt::MouseButton button) const override { if ((button == Qt::LeftButton || button == Qt::MiddleButton) && _joinChannel) { _joinChannel(_context); } } private: const not_null _channel; const QString _context; const Fn _openChannel; const Fn _joinChannel; }; class CachedPageMediaRuntime final : public Markdown::MediaRuntime { public: CachedPageMediaRuntime( not_null session, not_null page, Fn openChannel, Fn joinChannel) : _session(session) , _page(page) , _openChannel(std::move(openChannel)) , _joinChannel(std::move(joinChannel)) { } [[nodiscard]] std::shared_ptr resolveInlineImage( uint64 documentId, QSize size) const override { Q_UNUSED(size); const auto document = _session->data().document(DocumentId(documentId)); if (document->isNull()) { return nullptr; } return std::make_shared( document, fileOrigin()); } [[nodiscard]] std::shared_ptr resolvePhoto( uint64 photoId) const override { const auto photo = _session->data().photo(PhotoId(photoId)); if (photo->isNull()) { return nullptr; } return std::make_shared( _session, photo, fileOrigin()); } [[nodiscard]] std::shared_ptr resolveDocument( uint64 documentId) const override { const auto document = _session->data().document(DocumentId(documentId)); if (document->isNull()) { return nullptr; } return std::make_shared( _session, document, fileOrigin()); } [[nodiscard]] std::shared_ptr resolveMap( double latitude, double longitude, uint64 accessHash, QSize size, int zoom) const override { return std::make_shared( _session, fileOrigin(), latitude, longitude, accessHash, size, zoom); } [[nodiscard]] std::shared_ptr resolveChannel( uint64 channelId, const QString &username) const override { const auto channel = _session->data().channel(ChannelId(channelId)); subscribeToChannel(channelId, channel); return std::make_shared( channel, SerializeNativeIvChannelContext(channelId, username), _openChannel, _joinChannel); } [[nodiscard]] rpl::producer channelJoinedChanges() const override { return _channelJoinedChanges.events(); } [[nodiscard]] std::shared_ptr hostedMediaBlockFactory() const override { const auto controller = CurrentSessionController(_session); if (!controller || !_session->data().peerLoaded( PeerData::kServiceNotificationsId)) { return nullptr; } const auto history = _session->data().history( PeerData::kServiceNotificationsId); if (!history->peer->isUser()) { return nullptr; } return std::make_shared( base::make_weak(controller), [session = _session, pageUrl = _page->url, history]( Window::SessionController *controller, const Markdown::PreparedPhotoBlockData &prepared) { if (!controller || !prepared.viewerOpen || !prepared.urlOverride.isEmpty()) { return std::shared_ptr(); } const auto photo = session->data().photo(PhotoId(prepared.photoId)); if (photo->isNull()) { return std::shared_ptr(); } const auto item = AddIvNeutralHostMessage( history, pageUrl, nullptr, photo); auto descriptor = Markdown::IvHistoryViewMediaDescriptor(); descriptor.stableId = prepared.id.value; descriptor.kind = Markdown::IvHistoryViewMediaKind::Photo; descriptor.copyText = u"Photo"_q; descriptor.layoutHint = QSize(prepared.width, prepared.height); descriptor.session = &session->data(); descriptor.item = item; descriptor.mediaFactory = [photo]( not_null view) { return std::make_unique( view, view->data(), photo, false); }; descriptor.photo = std::make_shared( session, photo, ::Data::FileOriginWebPage{ pageUrl }); return Markdown::CreateIvHistoryViewMediaBlock( controller, std::move(descriptor)); }, [session = _session, pageUrl = _page->url, history]( Window::SessionController *controller, const Markdown::PreparedVideoBlockData &prepared) { if (!controller || prepared.media.kind != Markdown::PreparedMediaItemKind::Document) { return std::shared_ptr(); } const auto document = session->data().document( DocumentId(prepared.media.id)); if (document->isNull() || !CanHostNativeIvVideoDocument(document)) { return std::shared_ptr(); } const auto item = AddIvNeutralHostMessage( history, pageUrl, document); auto media = std::make_shared<::Data::MediaFile>( item, document, CachedPageVideoMediaArgs(session, document)); auto descriptor = Markdown::IvHistoryViewMediaDescriptor(); descriptor.stableId = prepared.id.value; descriptor.kind = Markdown::IvHistoryViewMediaKind::Document; descriptor.copyText = tr::lng_in_dlg_video(tr::now); descriptor.layoutHint = QSize( prepared.media.width, prepared.media.height); descriptor.session = &session->data(); descriptor.item = item; descriptor.mediaFactory = [media]( not_null view) { return media->createView( view, view->data()); }; descriptor.itemKeepAlive.push_back(base::take(media)); descriptor.document = std::make_shared( session, document, ::Data::FileOriginWebPage{ pageUrl }); return Markdown::CreateIvHistoryViewMediaBlock( controller, std::move(descriptor)); }, Markdown::IvHistoryViewMediaBlockFactory::AudioFactory(), [session = _session, pageUrl = _page->url, history]( Window::SessionController *controller, const Markdown::PreparedMapBlockData &prepared) { if (!controller) { return std::shared_ptr(); } const auto item = AddIvNeutralHostMessage(history, pageUrl); const auto point = CachedPageMapPoint( prepared.latitude, prepared.longitude, prepared.accessHash); const auto mapImage = std::make_shared<::Data::CloudImage>( session, CachedPageMapImageData( prepared.latitude, prepared.longitude, prepared.accessHash, QSize(prepared.width, prepared.height), prepared.zoom)); const auto mapImagePtr = mapImage.get(); auto descriptor = Markdown::IvHistoryViewMediaDescriptor(); descriptor.stableId = prepared.id.value; descriptor.kind = Markdown::IvHistoryViewMediaKind::Map; descriptor.copyText = tr::lng_maps_point(tr::now); descriptor.layoutHint = QSize(prepared.width, prepared.height); descriptor.session = &session->data(); descriptor.item = item; descriptor.mediaFactory = [mapImagePtr, point]( not_null view) { return std::make_unique( view, not_null{ mapImagePtr }, point); }; descriptor.keepAlive.push_back(mapImage); return Markdown::CreateIvHistoryViewMediaBlock( controller, std::move(descriptor)); }); } private: void subscribeToChannel( uint64 channelId, not_null channel) const { if (_channelJoinedSubscriptions.find(channelId) != end(_channelJoinedSubscriptions)) { return; } Info::Profile::AmInChannelValue(channel) | rpl::on_next([=](bool) { _channelJoinedChanges.fire_copy(channelId); }, _channelJoinedSubscriptions[channelId]); } [[nodiscard]] ::Data::FileOrigin fileOrigin() const { return ::Data::FileOriginWebPage{ _page->url }; } const not_null _session; const not_null _page; const Fn _openChannel; const Fn _joinChannel; mutable base::flat_map _channelJoinedSubscriptions; mutable rpl::event_stream _channelJoinedChanges; }; struct MarkdownMessageContext { ClickHandlerContext clickHandlerContext; base::weak_ptr sessionWindow; }; struct LocalMarkdownTarget { QString key; QString path; QString sourceName; QString fragment; }; [[nodiscard]] QString NormalizeLocalMarkdownFragment(QString fragment) { fragment = QString::fromUtf8( QByteArray::fromPercentEncoding(fragment.toUtf8())); fragment = fragment.trimmed().toLower(); while (fragment.startsWith(QChar('#'))) { fragment.remove(0, 1); } return fragment; } [[nodiscard]] LocalMarkdownTarget ParseLocalMarkdownTarget(QString path) { auto sourcePath = path; auto fragment = QString(); if (!QFileInfo(sourcePath).exists()) { const auto hash = sourcePath.lastIndexOf(QChar('#')); const auto candidate = (hash > 0) ? sourcePath.mid(0, hash) : QString(); if (!candidate.isEmpty() && QFileInfo(candidate).exists()) { fragment = NormalizeLocalMarkdownFragment(sourcePath.mid(hash + 1)); sourcePath = candidate; } } const auto info = QFileInfo(sourcePath); if (!info.exists()) { return { .key = path, .path = std::move(path), }; } auto result = LocalMarkdownTarget{ .key = info.absoluteFilePath(), .path = info.absoluteFilePath(), .sourceName = info.fileName(), .fragment = std::move(fragment), }; if (!result.fragment.isEmpty()) { result.path += u"#"_q + result.fragment; } return result; } [[nodiscard]] auto ExtractMarkdownMessageContext(const QVariant &context) { if (!context.isValid() || !context.canConvert()) { return std::optional(); } const auto clickHandlerContext = context.value(); return std::make_optional(MarkdownMessageContext{ .clickHandlerContext = clickHandlerContext, .sessionWindow = clickHandlerContext.sessionWindow, }); } [[nodiscard]] Main::Session *ResolveMarkdownSession( const MarkdownMessageContext &context) { if (const auto controller = context.sessionWindow.get()) { return &controller->session(); } return nullptr; } [[nodiscard]] HistoryItem *ResolveMarkdownItem( const MarkdownMessageContext &context) { const auto session = ResolveMarkdownSession(context); const auto itemId = context.clickHandlerContext.itemId; return (session && itemId) ? session->data().message(itemId) : nullptr; } [[nodiscard]] bool CanShareMarkdownItem(not_null item) { const auto peer = item->history()->peer; return peer->allowsForwarding() && !item->forbidsForward(); } [[nodiscard]] Markdown::OpenOptions PrepareLocalMarkdownOptions( QVariant context) { auto options = Markdown::OpenOptions{ .viewerKind = Markdown::ViewerKind::LocalFile, .clickHandlerContext = std::move(context), }; const auto messageContext = ExtractMarkdownMessageContext( options.clickHandlerContext); const auto item = messageContext ? ResolveMarkdownItem(*messageContext) : nullptr; if (item && CanShareMarkdownItem(not_null{ item })) { options.share = [context = *messageContext]( std::shared_ptr show) { const auto session = ResolveMarkdownSession(context); const auto itemId = context.clickHandlerContext.itemId; const auto current = (session && itemId) ? session->data().message(itemId) : nullptr; if (!show || !current || !CanShareMarkdownItem(not_null{ current })) { return; } FastShareMessage( Main::MakeSessionShow(show, not_null{ session }), not_null{ current }); }; } return options; } } // namespace class Shown final : public base::has_weak_ptr { public: Shown( not_null delegate, not_null session, not_null data, QString hash, Fn openChannel, Fn joinChannel); [[nodiscard]] bool showing( not_null session, not_null data) const; [[nodiscard]] bool showingFrom(not_null session) const; [[nodiscard]] bool activeFor(not_null session) const; [[nodiscard]] bool active() const; void moveTo(not_null data, QString hash); void update(not_null data); void showJoinedTooltip(); void minimize(); [[nodiscard]] rpl::producer events() const { return _events.events(); } [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } private: struct MapPreview { std::unique_ptr<::Data::CloudFile> file; QByteArray bytes; }; struct PartRequest { Webview::DataRequest request; QByteArray data; std::vector loaded; int64 offset = 0; }; struct FileStream { not_null document; std::unique_ptr<::Media::Streaming::Loader> loader; std::vector requests; std::string mime; rpl::lifetime lifetime; }; struct FileLoad { std::shared_ptr<::Data::DocumentMedia> media; std::vector requests; }; void prepare(not_null data, const QString &hash); void createController(); void createMarkdownController( Markdown::MarkdownArticleContent content, QString title, QString initialFragment, not_null page); [[nodiscard]] Markdown::OpenOptions markdownOpenOptions( QString initialFragment, not_null page) const; void showWindowed(Prepared result, Source source, bool refresh); void showHtmlWindowed(Prepared result, bool refresh); void showMarkdownWindowed( Markdown::MarkdownArticleContent content, QString title, QString initialFragment, not_null page); [[nodiscard]] ShareBoxResult shareBox(ShareBoxDescriptor &&descriptor); [[nodiscard]] std::shared_ptr createMediaRuntime( not_null page) const; [[nodiscard]] bool activateMarkdownMedia( const Markdown::MediaActivation &activation, Qt::MouseButton button, const QVariant &clickHandlerContext) const; [[nodiscard]] ::Data::FileOrigin fileOrigin( not_null page) const; void streamPhoto(QStringView idWithPageId, Webview::DataRequest request); void streamFile(QStringView idWithPageId, Webview::DataRequest request); void streamFile(FileStream &file, Webview::DataRequest request); void processPartInFile( FileStream &file, ::Media::Streaming::LoadedPart &&part); bool finishRequestWithPart( PartRequest &request, const ::Media::Streaming::LoadedPart &part); void streamMap(QString params, Webview::DataRequest request); void sendEmbed(QByteArray hash, Webview::DataRequest request); void fillChannelJoinedValues(const Prepared &result); void fillEmbeds(base::flat_map added); void subscribeToDocuments(); [[nodiscard]] QByteArray readFile( const std::shared_ptr<::Data::DocumentMedia> &media); void requestDone( Webview::DataRequest request, QByteArray bytes, std::string mime, int64 offset = 0, int64 total = 0); void requestFail(Webview::DataRequest request); const not_null _delegate; const not_null _session; const Fn _openChannel; const Fn _joinChannel; std::shared_ptr _show; QString _id; std::unique_ptr _controller; std::unique_ptr _markdownController; base::flat_map _streams; base::flat_map _files; base::flat_map> _inChannelValues; bool _preparing = false; base::flat_map _embeds; base::flat_map _maps; std::vector _resources; rpl::event_stream _events; rpl::lifetime _documentLifetime; rpl::lifetime _lifetime; }; class TonSite final : public base::has_weak_ptr { public: TonSite(not_null delegate, QString uri); [[nodiscard]] bool active() const; void moveTo(QString uri); void minimize(); [[nodiscard]] rpl::producer events() const { return _events.events(); } [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } private: void createController(); void showWindowed(); const not_null _delegate; QString _uri; std::unique_ptr _controller; rpl::event_stream _events; rpl::lifetime _lifetime; }; struct MarkdownShown { std::unique_ptr controller; }; Shown::Shown( not_null delegate, not_null session, not_null data, QString hash, Fn openChannel, Fn joinChannel) : _delegate(delegate) , _session(session) , _openChannel(std::move(openChannel)) , _joinChannel(std::move(joinChannel)) { prepare(data, hash); } void Shown::prepare(not_null data, const QString &hash) { const auto weak = base::make_weak(this); const auto source = data->source(); _preparing = true; const auto id = _id = data->id(); data->prepare({}, [=, source = source](Prepared result) { result.hash = hash; crl::on_main(weak, [=, source = source, result = std::move(result)]() mutable { result.url = id; if (_id != id || !_preparing) { return; } _preparing = false; fillChannelJoinedValues(result); fillEmbeds(std::move(result.embeds)); showWindowed(std::move(result), source, false); }); }); } void Shown::fillChannelJoinedValues(const Prepared &result) { for (const auto &id : result.channelIds) { const auto channelId = ChannelId(id.toLongLong()); const auto channel = _session->data().channel(channelId); if (!channel->isLoaded() && !channel->username().isEmpty()) { channel->session().api().request(MTPcontacts_ResolveUsername( MTP_flags(0), MTP_string(channel->username()), MTP_string() )).done([=](const MTPcontacts_ResolvedPeer &result) { channel->owner().processUsers(result.data().vusers()); channel->owner().processChats(result.data().vchats()); }).send(); } _inChannelValues[id] = Info::Profile::AmInChannelValue(channel); } } void Shown::fillEmbeds(base::flat_map added) { if (_embeds.empty()) { _embeds = std::move(added); } else { for (auto &[k, v] : added) { _embeds[k] = std::move(v); } } } ShareBoxResult Shown::shareBox(ShareBoxDescriptor &&descriptor) { class Show final : public Ui::Show { public: Show(QPointer parent, Fn lookup) : _parent(parent) , _lookup(lookup) { } void showOrHideBoxOrLayer( std::variant< v::null_t, object_ptr, std::unique_ptr> &&layer, Ui::LayerOptions options, anim::type animated) const override { using UniqueLayer = std::unique_ptr; using ObjectBox = object_ptr; const auto stack = _lookup(); if (!stack) { return; } else if (auto layerWidget = std::get_if(&layer)) { stack->showLayer(std::move(*layerWidget), options, animated); } else if (auto box = std::get_if(&layer)) { stack->showBox(std::move(*box), options, animated); } else { stack->hideAll(animated); } } not_null toastParent() const override { return _parent.data(); } bool valid() const override { return _lookup() != nullptr; } operator bool() const override { return valid(); } private: const QPointer _parent; const Fn _lookup; }; const auto url = descriptor.url; const auto wrap = descriptor.parent; struct State { Ui::LayerStackWidget *stack = nullptr; rpl::event_stream<> destroyRequests; }; const auto state = wrap->lifetime().make_state(); const auto weak = base::make_weak(wrap); const auto lookup = crl::guard(weak, [state] { return state->stack; }); const auto layer = Ui::CreateChild( wrap.get(), [=] { return std::make_shared(weak.get(), lookup); }); state->stack = layer; const auto show = layer->showFactory()(); layer->setHideByBackgroundClick(false); layer->move(0, 0); wrap->sizeValue( ) | rpl::on_next([=](QSize size) { layer->resize(size); }, layer->lifetime()); layer->hideFinishEvents( ) | rpl::filter([=] { return !!lookup(); // Last hide finish is sent from destructor. }) | rpl::on_next([=] { state->destroyRequests.fire({}); }, wrap->lifetime()); const auto waiting = layer->lifetime().make_state(); const auto focus = crl::guard(layer, [=] { const auto set = [=] { layer->window()->setFocus(); layer->setInnerFocus(); }; const auto handle = layer->window()->windowHandle(); if (!handle) { waiting->destroy(); return; } else if (QGuiApplication::focusWindow() == handle) { waiting->destroy(); set(); } else { *waiting = base::qt_signal_producer( qApp, &QGuiApplication::focusWindowChanged ) | rpl::filter([=](QWindow *focused) { const auto handle = layer->window()->windowHandle(); return handle && (focused == handle); }) | rpl::on_next([=] { waiting->destroy(); set(); }); layer->window()->activateWindow(); } }); auto result = ShareBoxResult{ .focus = focus, .hide = [=] { show->hideLayer(); }, .destroyRequests = state->destroyRequests.events(), }; FastShareLink(Main::MakeSessionShow(show, _session), url); return result; } void Shown::createController() { Expects(!_controller); const auto showShareBox = [=](ShareBoxDescriptor &&descriptor) { return shareBox(std::move(descriptor)); }; _controller = std::make_unique( _delegate, std::move(showShareBox)); _controller->events( ) | rpl::start_to_stream(_events, _controller->lifetime()); _controller->dataRequests( ) | rpl::on_next([=](Webview::DataRequest request) { const auto requested = QString::fromStdString(request.id); const auto id = QStringView(requested); if (id.startsWith(u"photo/")) { streamPhoto(id.mid(6), std::move(request)); } else if (id.startsWith(u"document/"_q)) { streamFile(id.mid(9), std::move(request)); } else if (id.startsWith(u"map/"_q)) { streamMap(id.mid(4).toUtf8(), std::move(request)); } else if (id.startsWith(u"html/"_q)) { sendEmbed(id.mid(5).toUtf8(), std::move(request)); } }, _controller->lifetime()); } Markdown::OpenOptions Shown::markdownOpenOptions( QString initialFragment, not_null page) const { const auto clickHandlerContext = std::make_shared(); auto options = Markdown::OpenOptions{ .sourceName = page->displayedSiteName(), .sourceUrl = page->url, .initialFragment = std::move(initialFragment), .viewerKind = Markdown::ViewerKind::InstantView, .clickHandlerContextRef = clickHandlerContext, .activateMedia = [=]( const Markdown::MediaActivation &activation, Qt::MouseButton button) { return activateMarkdownMedia(activation, button, *clickHandlerContext); }, }; if (!page->url.isEmpty()) { options.share = [=, url = page->url](std::shared_ptr show) { if (!show) { return; } FastShareLink(Main::MakeSessionShow(show, _session), url); }; } return options; } void Shown::createMarkdownController( Markdown::MarkdownArticleContent content, QString title, QString initialFragment, not_null page) { Expects(!_markdownController); auto options = markdownOpenOptions(std::move(initialFragment), page); _markdownController = std::make_unique( _delegate, std::move(content), std::move(title), nullptr, std::move(options)); _markdownController->events() | rpl::on_next([=](Markdown::Event event) { using FromType = Markdown::Event::Type; using ToType = Controller::Event::Type; switch (event.type) { case FromType::Close: _events.fire({ .type = ToType::Close }); break; case FromType::Quit: _events.fire({ .type = ToType::Quit }); break; case FromType::OpenFile: break; } }, _markdownController->lifetime()); Q_UNUSED(page); } void Shown::showWindowed(Prepared result, Source source, bool refresh) { const auto page = _session->data().webpage(result.pageId); auto native = Markdown::TryPrepareNativeInstantView({ .source = &source, .mediaRuntime = createMediaRuntime(page), }); if (!native.supported()) { showHtmlWindowed(std::move(result), refresh); return; } showMarkdownWindowed( std::move(native.content), std::move(result.name), std::move(result.hash), page); } void Shown::showHtmlWindowed(Prepared result, bool refresh) { _markdownController = nullptr; const auto hadController = (_controller != nullptr); if (!_controller) { createController(); } if (refresh && hadController) { _controller->update(std::move(result)); } else { _controller->show( _session->local().resolveStorageIdOther(), std::move(result), base::duplicate(_inChannelValues)); } } void Shown::showMarkdownWindowed( Markdown::MarkdownArticleContent content, QString title, QString initialFragment, not_null page) { _controller = nullptr; auto options = markdownOpenOptions(std::move(initialFragment), page); if (_markdownController) { _markdownController->update( std::move(content), std::move(title), std::move(options)); } else { createMarkdownController( std::move(content), std::move(title), std::move(initialFragment), page); } } std::shared_ptr Shown::createMediaRuntime( not_null page) const { return std::make_shared( _session, page, _openChannel, _joinChannel); } bool Shown::activateMarkdownMedia( const Markdown::MediaActivation &activation, Qt::MouseButton button, const QVariant &clickHandlerContext) const { if (button != Qt::LeftButton && button != Qt::MiddleButton) { return false; } switch (activation.kind) { case Markdown::MediaActivationKind::None: return false; case Markdown::MediaActivationKind::ExternalUrl: if (activation.url.isEmpty()) { return false; } HiddenUrlClickHandler::Open(activation.url, clickHandlerContext); return true; case Markdown::MediaActivationKind::Photo: if (!activation.photo) { return false; } activation.photo->open(button); return true; case Markdown::MediaActivationKind::Document: if (!activation.document) { return false; } activation.document->open(button); return true; case Markdown::MediaActivationKind::OpenChannel: if (!activation.channel) { return false; } activation.channel->open(button); return true; case Markdown::MediaActivationKind::JoinChannel: if (!activation.channel) { return false; } activation.channel->join(button); return true; } return false; } ::Data::FileOrigin Shown::fileOrigin(not_null page) const { return ::Data::FileOriginWebPage{ page->url }; } void Shown::streamPhoto( QStringView idWithPageId, Webview::DataRequest request) { using namespace Data; const auto parts = idWithPageId.split('/'); if (parts.size() != 2) { requestFail(std::move(request)); return; } const auto photo = _session->data().photo(parts[0].toULongLong()); const auto page = _session->data().webpage(parts[1].toULongLong()); if (photo->isNull() || page->url.isEmpty()) { requestFail(std::move(request)); return; } const auto media = photo->createMediaView(); media->wanted(PhotoSize::Large, fileOrigin(page)); const auto check = [=] { if (!media->loaded() && !media->owner()->failed(PhotoSize::Large)) { return false; } requestDone( request, media->imageBytes(PhotoSize::Large), "image/jpeg"); return true; }; if (!check()) { photo->session().downloaderTaskFinished( ) | rpl::filter( check ) | rpl::take(1) | rpl::start(_controller->lifetime()); } } void Shown::streamFile( QStringView idWithPageId, Webview::DataRequest request) { using namespace Data; const auto parts = idWithPageId.split('/'); if (parts.size() != 2) { requestFail(std::move(request)); return; } const auto documentId = DocumentId(parts[0].toULongLong()); const auto i = _streams.find(documentId); if (i != end(_streams)) { streamFile(i->second, std::move(request)); return; } const auto document = _session->data().document(documentId); const auto page = _session->data().webpage(parts[1].toULongLong()); if (page->url.isEmpty()) { requestFail(std::move(request)); return; } auto loader = document->createStreamingLoader(fileOrigin(page), false); if (!loader) { if (document->size >= Storage::kMaxFileInMemory) { requestFail(std::move(request)); } else { auto media = document->createMediaView(); if (const auto content = readFile(media); !content.isEmpty()) { requestDone( std::move(request), content, document->mimeString().toStdString()); } else { subscribeToDocuments(); auto &file = _files[documentId]; file.media = std::move(media); file.requests.push_back(std::move(request)); document->forceToCache(true); document->save(fileOrigin(page), QString()); } } return; } auto &file = _streams.emplace( documentId, FileStream{ .document = document, .loader = std::move(loader), .mime = document->mimeString().toStdString(), }).first->second; file.loader->parts( ) | rpl::on_next([=](::Media::Streaming::LoadedPart &&part) { const auto i = _streams.find(documentId); Assert(i != end(_streams)); processPartInFile(i->second, std::move(part)); }, file.lifetime); streamFile(file, std::move(request)); } void Shown::streamFile(FileStream &file, Webview::DataRequest request) { constexpr auto kPart = ::Media::Streaming::Loader::kPartSize; const auto size = file.document->size; const auto last = int((size + kPart - 1) / kPart); const auto from = int(std::min(int64(request.offset), size) / kPart); const auto till = (request.limit > 0) ? std::min(int64(request.offset + request.limit), size) : size; const auto parts = std::min( int((till + kPart - 1) / kPart) - from, kMaxLoadParts); //auto base = IvBaseCacheKey(document); const auto length = std::min((from + parts) * kPart, size) - from * kPart; file.requests.push_back(PartRequest{ .request = std::move(request), .data = QByteArray(length, 0), .loaded = std::vector(parts, false), .offset = from * kPart, }); file.loader->resetPriorities(); const auto load = std::min(from + kKeepLoadingParts, last) - from; for (auto i = 0; i != load; ++i) { file.loader->load((from + i) * kPart); } } void Shown::subscribeToDocuments() { if (_documentLifetime) { return; } _documentLifetime = _session->data().documentLoadProgress( ) | rpl::filter([=](not_null document) { return !document->loading(); }) | rpl::on_next([=](not_null document) { const auto i = _files.find(document->id); if (i == end(_files)) { return; } auto requests = base::take(i->second.requests); const auto content = readFile(i->second.media); _files.erase(i); if (!content.isEmpty()) { for (auto &request : requests) { requestDone( std::move(request), content, document->mimeString().toStdString()); } } else { for (auto &request : requests) { requestFail(std::move(request)); } } }); } QByteArray Shown::readFile( const std::shared_ptr<::Data::DocumentMedia> &media) { return Lottie::ReadContent(media->bytes(), media->owner()->filepath()); } void Shown::processPartInFile( FileStream &file, ::Media::Streaming::LoadedPart &&part) { for (auto i = begin(file.requests); i != end(file.requests);) { if (finishRequestWithPart(*i, part)) { auto done = base::take(*i); i = file.requests.erase(i); requestDone( std::move(done.request), done.data, file.mime, done.offset, file.document->size); } else { ++i; } } } bool Shown::finishRequestWithPart( PartRequest &request, const ::Media::Streaming::LoadedPart &part) { const auto offset = part.offset; if (offset == ::Media::Streaming::LoadedPart::kFailedOffset) { request.data = QByteArray(); return true; } else if (offset < request.offset || offset >= request.offset + request.data.size()) { return false; } constexpr auto kPart = ::Media::Streaming::Loader::kPartSize; const auto copy = std::min( int(part.bytes.size()), int(request.data.size() - (offset - request.offset))); const auto index = (offset - request.offset) / kPart; Assert(index < request.loaded.size()); if (request.loaded[index]) { return false; } request.loaded[index] = true; memcpy( request.data.data() + index * kPart, part.bytes.constData(), copy); return !ranges::contains(request.loaded, false); } void Shown::streamMap(QString params, Webview::DataRequest request) { using namespace ::Data; const auto parts = params.split(u'&'); if (parts.size() != 3) { requestFail(std::move(request)); return; } const auto point = GeoPointFromId(parts[0].toUtf8()); const auto size = parts[1].split(','); const auto zoom = parts[2].toInt(); if (size.size() != 2) { requestFail(std::move(request)); return; } const auto location = GeoPointLocation{ .lat = point.lat, .lon = point.lon, .access = point.access, .width = size[0].toInt(), .height = size[1].toInt(), .zoom = std::max(zoom, kGeoPointZoomMin), .scale = kGeoPointScale, }; const auto prepared = ImageWithLocation{ .location = ImageLocation( { location }, location.width, location.height) }; auto &preview = _maps.emplace(params, MapPreview()).first->second; preview.file = std::make_unique(); UpdateCloudFile( *preview.file, prepared, _session->data().cache(), kImageCacheTag, [=](FileOrigin origin) { /* restartLoader not used here */ }); const auto autoLoading = false; const auto finalCheck = [=] { return true; }; const auto done = [=](QByteArray bytes) { const auto i = _maps.find(params); Assert(i != end(_maps)); i->second.bytes = std::move(bytes); requestDone(request, i->second.bytes, "image/png"); }; LoadCloudFile( _session, *preview.file, FileOrigin(), LoadFromCloudOrLocal, autoLoading, kImageCacheTag, finalCheck, done, [=](bool) { done("failed..."); }); } void Shown::sendEmbed(QByteArray hash, Webview::DataRequest request) { const auto i = _embeds.find(hash); if (i != end(_embeds)) { requestDone(std::move(request), i->second, "text/html; charset=utf-8"); } else { requestFail(std::move(request)); } } void Shown::requestDone( Webview::DataRequest request, QByteArray bytes, std::string mime, int64 offset, int64 total) { if (bytes.isEmpty() && mime.empty()) { requestFail(std::move(request)); return; } crl::on_main([ done = std::move(request.done), data = std::move(bytes), mime = std::move(mime), offset, total ] { using namespace Webview; done({ .stream = std::make_unique(data, mime), .streamOffset = offset, .totalSize = total, }); }); } void Shown::requestFail(Webview::DataRequest request) { crl::on_main([done = std::move(request.done)] { done({}); }); } bool Shown::showing( not_null session, not_null data) const { return showingFrom(session) && (_id == data->id()); } bool Shown::showingFrom(not_null session) const { return (_session == session); } bool Shown::activeFor(not_null session) const { return showingFrom(session) && (_controller || _markdownController); } bool Shown::active() const { return (_controller && _controller->active()) || (_markdownController && _markdownController->active()); } void Shown::moveTo(not_null data, QString hash) { prepare(data, hash); } void Shown::update(not_null data) { const auto weak = base::make_weak(this); const auto source = data->source(); const auto id = data->id(); data->prepare({}, [=, source = source](Prepared result) { crl::on_main(weak, [=, source = source, result = std::move(result)]() mutable { result.url = id; fillChannelJoinedValues(result); fillEmbeds(std::move(result.embeds)); showWindowed(std::move(result), source, true); }); }); } void Shown::showJoinedTooltip() { if (_controller) { _controller->showJoinedTooltip(); } else if (_markdownController) { _markdownController->showJoinedTooltip(); } } void Shown::minimize() { if (_controller) { _controller->minimize(); } else if (_markdownController) { _markdownController->minimize(); } } TonSite::TonSite(not_null delegate, QString uri) : _delegate(delegate) , _uri(uri) { showWindowed(); } void TonSite::createController() { Expects(!_controller); const auto showShareBox = [=](ShareBoxDescriptor &&descriptor) { return ShareBoxResult(); }; _controller = std::make_unique( _delegate, std::move(showShareBox)); _controller->events( ) | rpl::start_to_stream(_events, _controller->lifetime()); } void TonSite::showWindowed() { if (!_controller) { createController(); } _controller->showTonSite(Storage::TonSiteStorageId(), _uri); } bool TonSite::active() const { return _controller && _controller->active(); } void TonSite::moveTo(QString uri) { _controller->showTonSite({}, uri); } void TonSite::minimize() { if (_controller) { _controller->minimize(); } } Instance::Instance(not_null delegate) : _delegate(delegate) { } Instance::~Instance() = default; void Instance::show( not_null controller, not_null data, QString hash) { show(controller->uiShow(), data, hash); } void Instance::show( std::shared_ptr show, not_null data, QString hash) { this->show(&show->session(), data, hash); } void Instance::show( not_null session, not_null data, QString hash) { if (Platform::IsMac()) { // Otherwise IV is not visible under the media viewer. Core::App().hideMediaView(); } if (Core::App().settings().normalizeIvZoom()) { Core::App().saveSettingsDelayed(); } const auto guard = gsl::finally([&] { requestFull(session, data->id()); }); if (_shown && _shownSession == session) { _shown->moveTo(data, hash); return; } _shown = std::make_unique( _delegate, session, data, hash, [=](QString context) { processOpenChannel(context); }, [=](QString context) { processJoinChannel(context); }); _shownSession = session; _shown->events() | rpl::on_next([=](Controller::Event event) { using Type = Controller::Event::Type; const auto lower = event.url.toLower(); const auto urlChecked = lower.startsWith("http://") || lower.startsWith("https://"); const auto tonsite = lower.startsWith("tonsite://"); switch (event.type) { case Type::Close: _shown = nullptr; break; case Type::Quit: Shortcuts::Launch(Shortcuts::Command::Quit); break; case Type::OpenChannel: processOpenChannel(event.context); break; case Type::JoinChannel: processJoinChannel(event.context); break; case Type::OpenLinkExternal: if (urlChecked) { File::OpenUrl(event.url); closeAll(); } else if (tonsite) { showTonSite(event.url); } break; case Type::OpenMedia: if (const auto window = Core::App().activeWindow()) { const auto current = window->sessionController(); const auto controller = (current && ¤t->session() == _shownSession) ? current : nullptr; const auto item = (HistoryItem*)nullptr; const auto topicRootId = MsgId(0); const auto monoforumPeerId = PeerId(0); if (event.context.startsWith("-photo")) { const auto id = event.context.mid(6).toULongLong(); const auto photo = _shownSession->data().photo(id); if (!photo->isNull()) { window->openInMediaView({ controller, photo, item, topicRootId, monoforumPeerId }); } } else if (event.context.startsWith("-video")) { const auto id = event.context.mid(6).toULongLong(); const auto video = _shownSession->data().document(id); if (!video->isNull()) { window->openInMediaView({ controller, video, item, topicRootId, monoforumPeerId }); } } } break; case Type::OpenPage: case Type::OpenLink: { if (tonsite) { showTonSite(event.url); break; } else if (!urlChecked) { break; } const auto session = _shownSession; const auto url = event.url; auto &requested = _fullRequested[session][url]; requested.lastRequestedAt = crl::now(); session->api().request(MTPmessages_GetWebPage( MTP_string(url), MTP_int(requested.hash) )).done([=](const MTPmessages_WebPage &result) { const auto page = processReceivedPage(session, url, result); if (page && page->iv) { const auto parts = event.url.split('#'); const auto hash = (parts.size() > 1) ? parts[1] : u""_q; this->show(_shownSession, page->iv.get(), hash); } else { UrlClickHandler::Open(event.url); } }).fail([=] { UrlClickHandler::Open(event.url); }).send(); } break; case Type::Report: if (const auto controller = _shownSession->tryResolveWindow()) { controller->window().activate(); controller->showPeerByLink(Window::PeerByLinkInfo{ .usernameOrId = "previews", .resolveType = Window::ResolveType::BotStart, .startToken = ("webpage" + QString::number(event.context.toULongLong())), }); } break; } }, _shown->lifetime()); session->changes().peerUpdates( ::Data::PeerUpdate::Flag::ChannelAmIn ) | rpl::on_next([=](const ::Data::PeerUpdate &update) { if (const auto channel = update.peer->asChannel()) { if (channel->amIn()) { const auto i = _joining.find(session); const auto value = not_null{ channel }; if (i != end(_joining) && i->second.remove(value)) { _shown->showJoinedTooltip(); } } } }, _shown->lifetime()); trackSession(session); } void Instance::trackSession(not_null session) { if (!_tracking.emplace(session).second) { return; } session->lifetime().add([=] { _tracking.remove(session); _joining.remove(session); _fullRequested.remove(session); _ivCache.remove(session); if (_ivRequestSession == session) { session->api().request(_ivRequestId).cancel(); _ivRequestSession = nullptr; _ivRequestUri = QString(); _ivRequestId = 0; } if (_shownSession == session) { _shownSession = nullptr; } if (_shown && _shown->showingFrom(session)) { _shown = nullptr; } }); } void Instance::openWithIvPreferred( not_null controller, QString uri, QVariant context) { auto my = context.value(); my.sessionWindow = controller; openWithIvPreferred( &controller->session(), uri, QVariant::fromValue(my)); } void Instance::openWithIvPreferred( not_null session, QString uri, QVariant context) { const auto openExternal = [=] { auto my = context.value(); my.ignoreIv = true; const auto updated = QVariant::fromValue(my); if (my.forceExternalUrlConfirmation) { HiddenUrlClickHandler::Open(uri, updated); } else { UrlClickHandler::Open(uri, updated); } }; const auto parts = uri.split('#'); if (parts.isEmpty() || parts[0].isEmpty()) { return; } else if (!ShowButton()) { return openExternal(); } trackSession(session); const auto hash = (parts.size() > 1) ? parts[1] : u""_q; const auto url = parts[0]; auto &cache = _ivCache[session]; if (const auto i = cache.find(url); i != end(cache)) { const auto page = i->second; if (page && page->iv) { auto my = context.value(); if (const auto window = my.sessionWindow.get()) { show(window, page->iv.get(), hash); } else { show(session, page->iv.get(), hash); } } else { openExternal(); } return; } else if (_ivRequestSession == session.get() && _ivRequestUri == uri) { return; } else if (_ivRequestId) { _ivRequestSession->api().request(_ivRequestId).cancel(); } const auto finish = [=](WebPageData *page) { Expects(_ivRequestSession == session); _ivRequestId = 0; _ivRequestUri = QString(); _ivRequestSession = nullptr; _ivCache[session][url] = page; openWithIvPreferred(session, uri, context); }; _ivRequestSession = session; _ivRequestUri = uri; auto &requested = _fullRequested[session][url]; requested.lastRequestedAt = crl::now(); _ivRequestId = session->api().request(MTPmessages_GetWebPage( MTP_string(url), MTP_int(requested.hash) )).done([=](const MTPmessages_WebPage &result) { finish(processReceivedPage(session, url, result)); }).fail([=] { finish(nullptr); }).send(); } void Instance::showTonSite( const QString &uri, QVariant context) { if (!Controller::IsGoodTonSiteUrl(uri)) { Ui::Toast::Show(tr::lng_iv_not_supported(tr::now)); return; } else if (Platform::IsMac()) { // Otherwise IV is not visible under the media viewer. Core::App().hideMediaView(); } if (_tonSite) { _tonSite->moveTo(uri); return; } _tonSite = std::make_unique(_delegate, uri); _tonSite->events() | rpl::on_next([=](Controller::Event event) { using Type = Controller::Event::Type; const auto lower = event.url.toLower(); const auto urlChecked = lower.startsWith("http://") || lower.startsWith("https://"); const auto tonsite = lower.startsWith("tonsite://"); switch (event.type) { case Type::Close: _tonSite = nullptr; break; case Type::Quit: Shortcuts::Launch(Shortcuts::Command::Quit); break; case Type::OpenLinkExternal: if (urlChecked) { File::OpenUrl(event.url); closeAll(); } else if (tonsite) { showTonSite(event.url); } break; case Type::OpenPage: case Type::OpenLink: if (urlChecked) { UrlClickHandler::Open(event.url); } else if (tonsite) { showTonSite(event.url); } break; } }, _tonSite->lifetime()); } bool Instance::showMarkdown( const QString &path, QVariant context) { const auto target = ParseLocalMarkdownTarget(path); auto options = PrepareLocalMarkdownOptions(context); if (!target.sourceName.isEmpty()) { options.sourceName = target.sourceName; options.sourcePath = target.key; } options.initialFragment = target.fragment; auto i = _markdowns.find(target.key); if (i == end(_markdowns)) { if (auto controller = Markdown::TryOpenLocalFile( _delegate, target.path, std::move(options))) { controller->events() | rpl::on_next([=](Markdown::Event event) { using Type = Markdown::Event::Type; switch (event.type) { case Type::Close: _markdowns.take(target.key); break; case Type::Quit: Shortcuts::Launch(Shortcuts::Command::Quit); break; // Don't try opening markdown links inside markdown viewer, // messenger-provided markdown files should know nothing // about other local files and their paths. // //case Type::OpenFile: // if (!showMarkdown(event.url, event.context)) { // DEBUG_LOG(("Native Markdown IV: " // "failed local markdown link: %1" // ).arg(event.url)); // } // break; } }, controller->lifetime()); i = _markdowns.emplace(target.key, std::move(controller)).first; } else { return false; } } else { i->second->updateOptions(std::move(options)); } i->second->activate(); return true; } void Instance::requestFull( not_null session, const QString &id) { if (!_tracking.contains(session)) { return; } auto &requested = _fullRequested[session][id]; const auto last = requested.lastRequestedAt; const auto now = crl::now(); if (last && (now - last) < kAllowPageReloadAfter) { return; } requested.lastRequestedAt = now; session->api().request(MTPmessages_GetWebPage( MTP_string(id), MTP_int(requested.hash) )).done([=](const MTPmessages_WebPage &result) { const auto page = processReceivedPage(session, id, result); if (page && page->iv && _shown && _shownSession == session) { _shown->update(page->iv.get()); } }).send(); } WebPageData *Instance::processReceivedPage( not_null session, const QString &url, const MTPmessages_WebPage &result) { const auto &data = result.data(); const auto owner = &session->data(); owner->processUsers(data.vusers()); owner->processChats(data.vchats()); auto &requested = _fullRequested[session][url]; const auto &mtp = data.vwebpage(); mtp.match([&](const MTPDwebPageNotModified &data) { const auto page = requested.page; if (const auto views = data.vcached_page_views()) { if (page && page->iv) { page->iv->updateCachedViews(views->v); } } }, [&](const MTPDwebPage &data) { requested.hash = data.vhash().v; requested.page = owner->processWebpage(data).get(); }, [&](const auto &) { requested.page = owner->processWebpage(mtp).get(); }); return requested.page; } void Instance::processOpenChannel(const QString &context) { if (!_shownSession) { return; } const auto parsed = ParseNativeIvChannelContext(context); if (const auto channelId = ChannelId(parsed.channelId)) { const auto channel = _shownSession->data().channel(channelId); if (channel->isLoaded()) { if (const auto controller = _shownSession->tryResolveWindow(channel)) { controller->showPeerHistory(channel); _shown = nullptr; } } else if (const auto username = ResolveNativeIvChannelUsername( channel->username(), parsed.username); !username.isEmpty()) { if (const auto controller = _shownSession->tryResolveWindow(channel)) { controller->showPeerByLink({ .usernameOrId = username, }); _shown = nullptr; } } } } void Instance::processJoinChannel(const QString &context) { if (!_shownSession) { return; } const auto parsed = ParseNativeIvChannelContext(context); if (const auto channelId = ChannelId(parsed.channelId)) { const auto channel = _shownSession->data().channel(channelId); _joining[_shownSession].emplace(channel); if (channel->isLoaded()) { _shownSession->api().joinChannel(channel); } else if (const auto username = ResolveNativeIvChannelUsername( channel->username(), parsed.username); !username.isEmpty()) { if (const auto controller = _shownSession->tryResolveWindow(channel)) { controller->showPeerByLink({ .usernameOrId = username, .joinChannel = true, }); } } } } bool Instance::hasActiveWindow(not_null session) const { return _shown && _shown->activeFor(session); } bool Instance::closeActive() { if (_shown && _shown->active()) { _shown = nullptr; return true; } else if (_tonSite && _tonSite->active()) { _tonSite = nullptr; return true; } for (auto &[key, controller] : _markdowns) { if (controller->active()) { _markdowns.take(key); return true; } } return false; } bool Instance::minimizeActive() { if (_shown && _shown->active()) { _shown->minimize(); return true; } else if (_tonSite && _tonSite->active()) { _tonSite->minimize(); return true; } return false; } void Instance::closeAll() { _shown = nullptr; _tonSite = nullptr; } bool PreferForUri(const QString &uri) { const auto url = QUrl(uri); const auto host = url.host().toLower(); const auto path = url.path().toLower(); return (host == u"telegra.ph"_q) || (host == u"te.legra.ph"_q) || (host == u"graph.org"_q) || (host == u"telegram.org"_q && (path.startsWith(u"/faq"_q) || path.startsWith(u"/privacy"_q) || path.startsWith(u"/blog"_q))); } } // namespace Iv