diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index fe6d3752c..4eaf2ec22 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -52,7 +52,7 @@ jobs: - "DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION" env: - UPLOAD_ARTIFACT: "false" + UPLOAD_ARTIFACT: "true" steps: - name: Get repository name. @@ -115,8 +115,8 @@ jobs: if: env.UPLOAD_ARTIFACT == 'true' run: | cd $REPO_NAME/out/Debug - mkdir artifact - mv {Telegram,Updater} artifact/ + sudo mkdir artifact + sudo mv {Telegram,Updater} artifact/ - uses: actions/upload-artifact@master if: env.UPLOAD_ARTIFACT == 'true' name: Upload artifact. diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 61561be9f..8271664c0 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -47,7 +47,7 @@ jobs: defines: - "" env: - UPLOAD_ARTIFACT: "false" + UPLOAD_ARTIFACT: "true" ONLY_CACHE: "false" PREPARE_PATH: "Telegram/build/prepare/prepare.py" diff --git a/.github/workflows/mac_packaged.yml b/.github/workflows/mac_packaged.yml index 51ba9917a..8ed4ffca1 100644 --- a/.github/workflows/mac_packaged.yml +++ b/.github/workflows/mac_packaged.yml @@ -50,7 +50,7 @@ jobs: env: GIT: "https://github.com" OPENALDIR: "/usr/local/opt/openal-soft" - UPLOAD_ARTIFACT: "false" + UPLOAD_ARTIFACT: "true" ONLY_CACHE: "false" MANUAL_CACHING: "1" AUTO_CACHING: "1" diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index c2e5dbafe..0c2bc4148 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-20.04 env: - UPLOAD_ARTIFACT: "false" + UPLOAD_ARTIFACT: "true" steps: - name: Clone. diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index b42906c7c..f8332c36f 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -50,7 +50,7 @@ jobs: generator: ["", "Ninja Multi-Config"] env: - UPLOAD_ARTIFACT: "false" + UPLOAD_ARTIFACT: "true" ONLY_CACHE: "false" PREPARE_PATH: "Telegram/build/prepare/prepare.py" diff --git a/.gitmodules b/.gitmodules index b489d4b7e..64928f01b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -76,6 +76,9 @@ [submodule "Telegram/lib_webview"] path = Telegram/lib_webview url = https://github.com/desktop-app/lib_webview.git +[submodule "Telegram/ThirdParty/jemalloc"] + path = Telegram/ThirdParty/jemalloc + url = https://github.com/jemalloc/jemalloc [submodule "Telegram/ThirdParty/dispatch"] path = Telegram/ThirdParty/dispatch url = https://github.com/apple/swift-corelibs-libdispatch diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index ae861a65a..4db06b26f 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1574,6 +1574,8 @@ PRIVATE window/window_lock_widgets.h window/window_main_menu.cpp window/window_main_menu.h + window/window_main_menu_helpers.cpp + window/window_main_menu_helpers.h window/window_media_preview.cpp window/window_media_preview.h window/window_peer_menu.cpp diff --git a/Telegram/Resources/animations/collectible_phone.tgs b/Telegram/Resources/animations/collectible_phone.tgs new file mode 100644 index 000000000..4e7284d5f Binary files /dev/null and b/Telegram/Resources/animations/collectible_phone.tgs differ diff --git a/Telegram/Resources/animations/collectible_username.tgs b/Telegram/Resources/animations/collectible_username.tgs new file mode 100644 index 000000000..f93fd6d74 Binary files /dev/null and b/Telegram/Resources/animations/collectible_username.tgs differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 77254d7a2..00e0bcf7c 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -19,6 +19,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_menu_set_status" = "Set Emoji Status"; "lng_menu_change_status" = "Change Emoji Status"; "lng_menu_my_stories" = "My Stories"; +"lng_menu_my_groups" = "My Groups"; +"lng_menu_my_channels" = "My Channels"; "lng_disable_notifications_from_tray" = "Disable notifications"; "lng_enable_notifications_from_tray" = "Enable notifications"; @@ -469,6 +471,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_bio_placeholder" = "Bio"; +"lng_collectible_username_title" = "{username} is a collectible username that belongs to"; +"lng_collectible_username_info" = "This username was bought on **Fragment** on {date} for {price}"; +"lng_collectible_username_copy" = "Copy Link"; +"lng_collectible_phone_title" = "{phone} is a collectible phone number that belongs to"; +"lng_collectible_phone_info" = "This phone number was bought on **Fragment** on {date} for {price}"; +"lng_collectible_phone_copy" = "Copy Phone Number"; +"lng_collectible_learn_more" = "Learn More"; + "lng_settings_section_info" = "My info"; "lng_settings_section_notify" = "Notifications"; @@ -1058,6 +1068,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_faq_button" = "Go to FAQ"; "lng_settings_ask_ok" = "Ask a Volunteer"; "lng_settings_faq" = "Telegram FAQ"; +"lng_settings_faq_link" = "https://telegram.org/faq#general-questions"; "lng_settings_features" = "Telegram Features"; "lng_settings_logout" = "Log Out"; "lng_sure_logout" = "Are you sure you want to log out?"; @@ -5040,6 +5051,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_channel_earn_learn_coin_title" = "What is {emoji} TON?"; "lng_channel_earn_learn_coin_about" = "TON is a blockchain platform and cryptocurrency that Telegram uses for its high speed and low commissions on transactions. {link}"; "lng_channel_earn_learn_close" = "Got it"; +"lng_channel_earn_learn_coin_link" = "https://telegram.org/blog/monetization-for-channels"; "lng_channel_earn_chart_top_hours" = "Ad impressions"; "lng_channel_earn_chart_revenue" = "Ad revenue"; "lng_channel_earn_chart_overriden_detail_currency" = "Revenue in TON"; diff --git a/Telegram/Resources/qrc/telegram/animations.qrc b/Telegram/Resources/qrc/telegram/animations.qrc index b63b6d15f..5b72c461f 100644 --- a/Telegram/Resources/qrc/telegram/animations.qrc +++ b/Telegram/Resources/qrc/telegram/animations.qrc @@ -22,5 +22,7 @@ ../../animations/hours.tgs ../../animations/phone.tgs ../../animations/chat_link.tgs + ../../animations/collectible_username.tgs + ../../animations/collectible_phone.tgs diff --git a/Telegram/Resources/uwp/AppX/AppxManifest.xml b/Telegram/Resources/uwp/AppX/AppxManifest.xml index 2627ef790..3488163e5 100644 --- a/Telegram/Resources/uwp/AppX/AppxManifest.xml +++ b/Telegram/Resources/uwp/AppX/AppxManifest.xml @@ -10,7 +10,7 @@ + Version="4.16.6.0" /> Telegram Desktop Telegram Messenger LLP diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 7585dfef0..07be79162 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,16,0,0 - PRODUCTVERSION 4,16,0,0 + FILEVERSION 4,16,6,0 + PRODUCTVERSION 4,16,6,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -62,10 +62,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop" - VALUE "FileVersion", "4.16.0.0" + VALUE "FileVersion", "4.16.6.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.16.0.0" + VALUE "ProductVersion", "4.16.6.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index ae31782f3..88221f475 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 4,16,0,0 - PRODUCTVERSION 4,16,0,0 + FILEVERSION 4,16,6,0 + PRODUCTVERSION 4,16,6,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -53,10 +53,10 @@ BEGIN BEGIN VALUE "CompanyName", "Radolyn Labs" VALUE "FileDescription", "AyuGram Desktop Updater" - VALUE "FileVersion", "4.16.0.0" + VALUE "FileVersion", "4.16.6.0" VALUE "LegalCopyright", "Copyright (C) 2014-2024" VALUE "ProductName", "AyuGram Desktop" - VALUE "ProductVersion", "4.16.0.0" + VALUE "ProductVersion", "4.16.6.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/api/api_peer_photo.cpp b/Telegram/SourceFiles/api/api_peer_photo.cpp index c70ef4491..9c8a470fa 100644 --- a/Telegram/SourceFiles/api/api_peer_photo.cpp +++ b/Telegram/SourceFiles/api/api_peer_photo.cpp @@ -34,7 +34,7 @@ namespace { constexpr auto kSharedMediaLimit = 100; -[[nodiscard]] SendMediaReady PreparePeerPhoto( +[[nodiscard]] std::shared_ptr PreparePeerPhoto( MTP::DcId dcId, PeerId peerId, QImage &&image) { @@ -80,24 +80,17 @@ constexpr auto kSharedMediaLimit = 100; MTPVector(), MTP_int(dcId)); - QString file, filename; - int64 filesize = 0; - QByteArray data; - - return SendMediaReady( - SendMediaType::Photo, - file, - filename, - filesize, - data, - id, - id, - u"jpg"_q, - peerId, - photo, - photoThumbs, - MTP_documentEmpty(MTP_long(0)), - jpeg); + auto result = MakePreparedFile({ + .id = id, + .type = SendMediaType::Photo, + }); + result->type = SendMediaType::Photo; + result->setFileData(jpeg); + result->thumbId = id; + result->thumbname = "thumb.jpg"; + result->photo = photo; + result->photoThumbs = photoThumbs; + return result; } [[nodiscard]] std::optional PrepareMtpMarkup( @@ -239,7 +232,7 @@ void PeerPhoto::upload( _api.instance().mainDcId(), peer->id, base::take(photo.image)); - _session->uploader().uploadMedia(fakeId, ready); + _session->uploader().upload(fakeId, ready); } } diff --git a/Telegram/SourceFiles/api/api_ringtones.cpp b/Telegram/SourceFiles/api/api_ringtones.cpp index 87522bf58..307ba580a 100644 --- a/Telegram/SourceFiles/api/api_ringtones.cpp +++ b/Telegram/SourceFiles/api/api_ringtones.cpp @@ -24,16 +24,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Api { namespace { -SendMediaReady PrepareRingtoneDocument( +std::shared_ptr PrepareRingtoneDocument( MTP::DcId dcId, const QString &filename, const QString &filemime, const QByteArray &content) { + const auto id = base::RandomValue(); auto attributes = QVector( 1, MTP_documentAttributeFilename(MTP_string(filename))); - const auto id = base::RandomValue(); - const auto document = MTP_document( + + auto result = MakePreparedFile({ + .id = id, + .type = SendMediaType::File, + }); + result->filename = filename; + result->content = content; + result->filesize = content.size(); + result->setFileData(content); + result->document = MTP_document( MTP_flags(0), MTP_long(id), MTP_long(0), @@ -45,21 +54,7 @@ SendMediaReady PrepareRingtoneDocument( MTPVector(), MTP_int(dcId), MTP_vector(std::move(attributes))); - - return SendMediaReady( - SendMediaType::File, - QString(), // filepath - filename, - content.size(), - content, - id, - 0, - QString(), - PeerId(), - MTP_photoEmpty(MTP_long(0)), - PreparedPhotoThumbs(), - document, - QByteArray()); + return result; } } // namespace @@ -102,7 +97,7 @@ void Ringtones::upload( _uploads.erase(already); } _uploads.emplace(fakeId, uploadedData); - _session->uploader().uploadMedia(fakeId, ready); + _session->uploader().upload(fakeId, ready); } void Ringtones::ready(const FullMsgId &msgId, const MTPInputFile &file) { diff --git a/Telegram/SourceFiles/api/api_sending.cpp b/Telegram/SourceFiles/api/api_sending.cpp index 72bd06b3d..d91943fe0 100644 --- a/Telegram/SourceFiles/api/api_sending.cpp +++ b/Telegram/SourceFiles/api/api_sending.cpp @@ -353,7 +353,7 @@ void FillMessagePostFlags( void SendConfirmedFile( not_null session, - const std::shared_ptr &file) { + const std::shared_ptr &file) { const auto isEditing = (file->type != SendMediaType::Audio) && (file->to.replaceMediaOf != 0); const auto newId = FullMsgId( diff --git a/Telegram/SourceFiles/api/api_sending.h b/Telegram/SourceFiles/api/api_sending.h index e17c66f3e..2fdbad843 100644 --- a/Telegram/SourceFiles/api/api_sending.h +++ b/Telegram/SourceFiles/api/api_sending.h @@ -14,7 +14,7 @@ class Session; class History; class PhotoData; class DocumentData; -struct FileLoadResult; +struct FilePrepareResult; namespace Api { @@ -40,6 +40,6 @@ void FillMessagePostFlags( void SendConfirmedFile( not_null session, - const std::shared_ptr &file); + const std::shared_ptr &file); } // namespace Api diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index 794088ed4..ab030fb57 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -813,7 +813,7 @@ QString ApiWrap::exportDirectStoryLink(not_null story) { const auto storyId = story->fullId(); const auto peer = story->peer(); const auto fallback = [&] { - const auto base = peer->userName(); + const auto base = peer->username(); const auto story = QString::number(storyId.story); const auto query = base + "/s/" + story; return session().createInternalLinkFull(query); diff --git a/Telegram/SourceFiles/boxes/background_preview_box.cpp b/Telegram/SourceFiles/boxes/background_preview_box.cpp index 3a9917719..a112c478a 100644 --- a/Telegram/SourceFiles/boxes/background_preview_box.cpp +++ b/Telegram/SourceFiles/boxes/background_preview_box.cpp @@ -593,11 +593,11 @@ void BackgroundPreviewBox::uploadForPeer(bool both) { const auto ready = Window::Theme::PrepareWallPaper( session->mainDcId(), _paper.localThumbnail()->original()); - const auto documentId = ready.id; + const auto documentId = ready->id; _uploadId = FullMsgId( session->userPeerId(), session->data().nextLocalMessageId()); - session->uploader().uploadMedia(_uploadId, ready); + session->uploader().upload(_uploadId, ready); if (_uploadLifetime) { return; } diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index f70c7b88d..c181168d1 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1044,3 +1044,33 @@ inviteForbiddenTitle: FlatLabel(boxTitle) { inviteForbiddenTitlePadding: margins(32px, 4px, 32px, 0px); inviteForbiddenLockBg: dialogsUnreadBgMuted; inviteForbiddenLockIcon: icon {{ "emoji/premium_lock", dialogsUnreadFg }}; + +collectibleIconDiameter: 72px; +collectibleIcon: 64px; +collectibleIconPadding: margins(24px, 32px, 24px, 12px); +collectibleHeader: FlatLabel(boxTitle) { + minWidth: 120px; + maxHeight: 0px; + align: align(top); +} +collectibleHeaderPadding: margins(24px, 16px, 24px, 12px); +collectibleOwnerPadding: margins(24px, 4px, 24px, 8px); +collectibleInfo: inviteForbiddenInfo; +collectibleInfoPadding: margins(24px, 12px, 24px, 12px); +collectibleInfoTonMargins: margins(0px, 3px, 0px, 0px); +collectibleMore: RoundButton(defaultActiveButton) { + height: 36px; + textTop: 9px; + radius: 6px; +} +collectibleMorePadding: margins(24px, 12px, 24px, 0px); +collectibleCopy: RoundButton(defaultLightButton) { + height: 36px; + textTop: 9px; + radius: 6px; +} +collectibleBox: Box(defaultBox) { + buttonPadding: margins(24px, 12px, 24px, 12px); + buttonHeight: 36px; + button: collectibleCopy; +} diff --git a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp index 9fcb6b032..7800e277e 100644 --- a/Telegram/SourceFiles/boxes/edit_privacy_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_privacy_box.cpp @@ -159,10 +159,7 @@ public: -> rpl::producer; private: - [[nodiscard]] std::unique_ptr createRow() const; - const not_null _session; - bool _premiums = false; rpl::event_stream<> _selectionChanged; rpl::event_stream _rowSelectionChanges; @@ -209,8 +206,7 @@ bool PremiumsRow::useForumLikeUserpic() const { TypesController::TypesController( not_null session, bool premiums) -: _session(session) -, _premiums(premiums) { +: _session(session) { } Main::Session &TypesController::session() const { @@ -331,6 +327,9 @@ auto PrivacyExceptionsBoxController::preparePremiumsRowList() _deselectOption = [=](PeerListRowId itemId) { if (const auto row = _typesDelegate->peerListFindRow(itemId)) { + if (itemId == kPremiumsRowId) { + _selected.premiums = false; + } _typesDelegate->peerListSetRowChecked(row, false); } }; diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 2430c583e..bda06d535 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -1039,11 +1039,9 @@ void PeerListContent::changeCheckState( not_null row, bool checked, anim::type animated) { - row->setChecked( - checked, - _st.item.checkbox, - animated, - [=] { updateRow(row); }); + row->setChecked(checked, _st.item.checkbox, animated, [=] { + updateRow(row); + }); } void PeerListContent::setRowHidden(not_null row, bool hidden) { @@ -1789,10 +1787,10 @@ crl::time PeerListContent::paintRow( if (row->isSearchResult() && !_mentionHighlight.isEmpty() && peer - && peer->userName().startsWith( + && peer->username().startsWith( _mentionHighlight, Qt::CaseInsensitive)) { - const auto username = peer->userName(); + const auto username = peer->username(); const auto availableWidth = statusw; auto highlightedPart = '@' + username.mid(0, _mentionHighlight.size()); auto grayedPart = username.mid(_mentionHighlight.size()); diff --git a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp index f8a7ea6e8..bae3d8a76 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_linked_chat_box.cpp @@ -100,7 +100,7 @@ void Controller::prepare() { return; } auto row = std::make_unique(chat); - const auto username = chat->userName(); + const auto username = chat->username(); row->setCustomStatus(!username.isEmpty() ? ('@' + username) : (chat->isChannel() && !chat->isMegagroup()) diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp index a6b442aca..6c19d4055 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp @@ -207,7 +207,7 @@ void ProcessFullPhoto( | UpdateFlag::Birthday) ) | rpl::map([=] { const auto user = peer->asUser(); - const auto username = peer->userName(); + const auto username = peer->username(); return PeerShortInfoFields{ .name = peer->name(), .phone = user ? Ui::FormatPhone(user->phone()) : QString(), diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 2a9c0d4b1..451957c21 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -322,7 +322,7 @@ void PublicsController::prepare() { auto &owner = _navigation->session().data(); for (const auto &chat : chats) { if (const auto peer = owner.processChat(chat)) { - if (!peer->isChannel() || peer->userName().isEmpty()) { + if (!peer->isChannel() || peer->username().isEmpty()) { continue; } appendRow(peer); @@ -346,7 +346,7 @@ void PublicsController::rowRightActionClicked(not_null row) { const auto text = textMethod( tr::now, lt_link, - peer->session().createInternalLink(peer->userName()), + peer->session().createInternalLink(peer->username()), lt_group, peer->name()); const auto confirmText = tr::lng_channels_too_much_public_revoke( @@ -389,7 +389,7 @@ std::unique_ptr PublicsController::createRow( auto result = std::make_unique(peer); result->setActionLink(tr::lng_channels_too_much_public_revoke(tr::now)); result->setCustomStatus( - _navigation->session().createInternalLink(peer->userName())); + _navigation->session().createInternalLink(peer->username())); return result; } diff --git a/Telegram/SourceFiles/boxes/stickers_box.cpp b/Telegram/SourceFiles/boxes/stickers_box.cpp index 01f6a1ed1..0597a8fa2 100644 --- a/Telegram/SourceFiles/boxes/stickers_box.cpp +++ b/Telegram/SourceFiles/boxes/stickers_box.cpp @@ -652,8 +652,8 @@ void StickersBox::prepare() { } if (const auto featured = _featured.widget()) { featured->setInstallSetCallback([=](uint64 setId) { - markAsInstalledCallback(setId); installCallback(setId); + markAsInstalledCallback(setId); }); featured->setRemoveSetCallback(markAsRemovedCallback); } @@ -1760,8 +1760,11 @@ void StickersBox::Inner::setActionDown(int newActionDown) { const auto &st = installedSet ? st::stickersTrendingInstalled : st::stickersTrendingAdd; + const auto buttonTextWidth = installedSet + ? _installedWidth + : _addWidth; auto rippleMask = Ui::RippleAnimation::RoundRectMask( - QSize(_addWidth - st.width, st.height), + QSize(buttonTextWidth - st.width, st.height), st::roundRadiusLarge); ensureRipple( st.ripple, @@ -1902,9 +1905,19 @@ void StickersBox::Inner::updateSelected() { selected = selectedIndex; local.setY(local.y() - _itemsTop - selectedIndex * _rowHeight); const auto row = _rows[selectedIndex].get(); - if (!_megagroupSet && (_isInstalledTab || (_section == Section::Featured) || !row->isInstalled() || row->isArchived() || row->removed)) { + if (!_megagroupSet + && (_isInstalledTab + || (_section == Section::Featured) + || !row->isInstalled() + || row->isArchived() + || row->removed)) { auto removeButton = (_isInstalledTab && !row->removed); - auto rect = myrtlrect(relativeButtonRect(removeButton, false)); + + const auto installedSetButton = !_isInstalledTab + && row->isInstalled() + && !row->isArchived() + && !row->removed; + auto rect = myrtlrect(relativeButtonRect(removeButton, installedSetButton)); actionSel = rect.contains(local) ? selectedIndex : -1; } else { actionSel = -1; @@ -1957,12 +1970,19 @@ void StickersBox::Inner::mouseReleaseEvent(QMouseEvent *e) { _mouse = e->globalPos(); updateSelected(); - if (_actionDown == _actionSel && _actionSel >= 0) { - const auto callback = _rows[_actionDown]->removed - ? _installSetCallback - : _removeSetCallback; + const auto down = _actionDown; + setActionDown(-1); + if (down == _actionSel && _actionSel >= 0) { + const auto row = _rows[down].get(); + const auto installedSet = row->isInstalled() + && !row->isArchived() + && !row->removed; + const auto callback = installedSet + ? _removeSetCallback + : _installSetCallback; if (callback) { - callback(_rows[_actionDown]->set->id); + row->ripple.reset(); + callback(row->set->id); } } else if (_dragging >= 0) { _rows[_dragging]->yadd.start(0.); @@ -1973,7 +1993,7 @@ void StickersBox::Inner::mouseReleaseEvent(QMouseEvent *e) { } _dragging = _started = -1; - } else if (pressed == _selected && _actionSel < 0 && _actionDown < 0) { + } else if (pressed == _selected && _actionSel < 0 && down < 0) { const auto selectedIndex = [&] { if (auto index = std::get_if(&_selected)) { return *index; @@ -1997,7 +2017,6 @@ void StickersBox::Inner::mouseReleaseEvent(QMouseEvent *e) { showSetByRow(*_megagroupSelectedSet); } } - setActionDown(-1); } void StickersBox::Inner::saveGroupSet(Fn done) { diff --git a/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp b/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp index a9f41b35a..02ae9cd68 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members_row.cpp @@ -612,9 +612,9 @@ void MembersRow::paintComplexStatusText( x += skip; availableWidth -= skip; const auto &font = st::normalFont; - const auto useAbout = (style == MembersRowStyle::Video) - ? false - : ((_state == State::RaisedHand && !_raisedHandStatus) + const auto useAbout = !_about.isEmpty() + && (style != MembersRowStyle::Video) + && ((_state == State::RaisedHand && !_raisedHandStatus) || (_state != State::RaisedHand && !_speaking)); if (!useAbout && _state != State::Invited diff --git a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp index ad59290c5..93c5ecf1e 100644 --- a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp +++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp @@ -587,11 +587,9 @@ void ChooseSourceProcess::setupGeometryWithParent( not_null parent) { _window->createWinId(); const auto parentScreen = [&] { - if (!::Platform::IsWayland()) { - if (const auto screen = QGuiApplication::screenAt( + if (const auto screen = QGuiApplication::screenAt( parent->geometry().center())) { - return screen; - } + return screen; } return parent->screen(); }(); diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index caf790d27..ac8b6f117 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_document.h" +#include "data/data_file_origin.h" #include "data/data_peer_values.h" #include "data/stickers/data_stickers.h" #include "data/stickers/data_custom_emoji.h" @@ -51,10 +52,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" -// AyuGram includes -#include "data/data_file_origin.h" -#include "qapplication.h" - +#include namespace ChatHelpers { namespace { @@ -62,6 +60,7 @@ namespace { constexpr auto kCollapsedRows = 3; constexpr auto kAppearDuration = 0.3; constexpr auto kCustomSearchLimit = 256; +constexpr auto kColorPickerDelay = crl::time(500); using Core::RecentEmojiId; using Core::RecentEmojiDocument; @@ -630,6 +629,15 @@ void EmojiListWidget::applyNextSearchQuery() { } } +void EmojiListWidget::showPreview() { + if (const auto over = std::get_if(&_pressed)) { + if (const auto custom = lookupCustomEmoji(over)) { + _show->showMediaPreview(custom->stickerSetOrigin(), custom); + _previewShown = true; + } + } +} + std::vector EmojiListWidget::collectPlainSearchResults() { return SearchEmoji(_searchQuery, _searchEmoji); } @@ -1125,7 +1133,7 @@ void EmojiListWidget::fillRecentMenu( const auto addAction = Ui::Menu::CreateAddActionCallback(menu); const auto over = OverEmoji{ section, index }; const auto emoji = lookupOverEmoji(&over); - const auto custom = lookupCustomEmoji(index, section); + const auto custom = lookupCustomEmoji(&over); if (custom && custom->sticker()) { const auto sticker = custom->sticker(); const auto emoji = sticker->alt; @@ -1498,6 +1506,11 @@ bool EmojiListWidget::checkPickerHide() { return false; } +DocumentData *EmojiListWidget::lookupCustomEmoji( + const OverEmoji *over) const { + return over ? lookupCustomEmoji(over->index, over->section) : nullptr; +} + DocumentData *EmojiListWidget::lookupCustomEmoji( int index, int section) const { @@ -1600,12 +1613,14 @@ void EmojiListWidget::mousePressEvent(QMouseEvent *e) { if (!Core::App().settings().hasChosenEmojiVariant(emoji)) { showPicker(); } else { - _showPickerTimer.callOnce(500); + _previewTimer.cancel(); + _showPickerTimer.callOnce(kColorPickerDelay); } + } else if (lookupCustomEmoji(over)) { + _showPickerTimer.cancel(); + _previewTimer.callOnce(QApplication::startDragTime()); } } - - _previewTimer.callOnce(QApplication::startDragTime()); } void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { @@ -1613,11 +1628,6 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { auto pressed = _pressed; setPressed(v::null); - if (_previewShown) { - _previewShown = false; - return; - } - _lastMousePos = e->globalPos(); if (!_picker->isHidden()) { if (_picker->rect().contains(_picker->mapFromGlobal(_lastMousePos))) { @@ -1640,7 +1650,10 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { _picker->hide(); } - if (v::is_null(_selected) || _selected != pressed) { + if (_previewShown) { + _previewShown = false; + return; + } else if (v::is_null(_selected) || _selected != pressed) { return; } @@ -1659,7 +1672,7 @@ void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { return; } selectEmoji(lookupChosen(emoji, over)); - } else if (const auto custom = lookupCustomEmoji(index, section)) { + } else if (const auto custom = lookupCustomEmoji(over)) { selectCustom(lookupChosen(custom, over)); } } else if (const auto set = std::get_if(&pressed)) { @@ -2492,8 +2505,10 @@ bool EmojiListWidget::eventHook(QEvent *e) { } void EmojiListWidget::updateSelected() { - if ((!v::is_null(_pressed) || !v::is_null(_pickerSelected)) && !_previewShown) { - return; + if (!v::is_null(_pressed) || !v::is_null(_pickerSelected)) { + if (!_previewShown) { + return; + } } auto newSelected = OverState{ v::null }; @@ -2542,7 +2557,7 @@ void EmojiListWidget::setSelected(OverState newSelected) { const auto hasSelection = !v::is_null(_selected); if (hasSelection && Core::App().settings().suggestEmoji()) { - Ui::Tooltip::Show(350, this); + Ui::Tooltip::Show(1000, this); } setCursor(hasSelection ? style::cur_pointer : style::cur_default); @@ -2552,34 +2567,16 @@ void EmojiListWidget::setSelected(OverState newSelected) { } else { _picker->showAnimated(); } - } - - if (_previewShown && _pressed != _selected) { + } else if (_previewShown && _pressed != _selected) { if (const auto over = std::get_if(&_selected)) { - _pressed = _selected; - - const auto section = over ? over->section : -1; - const auto index = over ? over->index : -1; - - if (const auto document = lookupCustomEmoji(index, section)) { - _show->showMediaPreview(document->stickerSetOrigin(), document); + if (const auto custom = lookupCustomEmoji(over)) { + _pressed = _selected; + _show->showMediaPreview(custom->stickerSetOrigin(), custom); } } } } -void EmojiListWidget::showPreview() { - if (const auto over = std::get_if(&_selected)) { - const auto section = over ? over->section : -1; - const auto index = over ? over->index : -1; - - if (const auto document = lookupCustomEmoji(index, section)) { - _show->showMediaPreview(document->stickerSetOrigin(), document); - _previewShown = true; - } - } -} - void EmojiListWidget::setPressed(OverState newPressed) { if (auto button = std::get_if(&_pressed)) { Assert(hasColorButton(button->section) diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h index 647a2c789..9f35f1963 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.h @@ -287,6 +287,8 @@ private: int index); [[nodiscard]] EmojiPtr lookupOverEmoji(const OverEmoji *over) const; + [[nodiscard]] DocumentData *lookupCustomEmoji( + const OverEmoji *over) const; [[nodiscard]] DocumentData *lookupCustomEmoji( int index, int section) const; @@ -371,6 +373,8 @@ private: DocumentId documentId, uint64 setId); + void showPreview(); + void applyNextSearchQuery(); void showPreview(); @@ -442,6 +446,8 @@ private: object_ptr _picker; base::Timer _showPickerTimer; + base::Timer _previewTimer; + bool _previewShown = false; rpl::event_stream _chosen; rpl::event_stream _customChosen; diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 15a7dc1f9..e064d1e2d 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -46,6 +46,7 @@ struct ClickHandlerContext { bool mayShowConfirmation = false; bool skipBotAutoLogin = false; bool botStartAutoSubmit = false; + bool ignoreIv = false; // Is filled from peer info. PeerData *peer = nullptr; }; diff --git a/Telegram/SourceFiles/core/file_utilities.cpp b/Telegram/SourceFiles/core/file_utilities.cpp index 26cc17279..1de1e028f 100644 --- a/Telegram/SourceFiles/core/file_utilities.cpp +++ b/Telegram/SourceFiles/core/file_utilities.cpp @@ -159,7 +159,7 @@ void Launch(const QString &filepath) { void ShowInFolder(const QString &filepath) { crl::on_main([=] { Ui::PreventDelayedActivation(); - if (Platform::IsLinux()) { + if (Platform::IsX11()) { // Hide mediaview to make other apps visible. Core::App().hideMediaView(); } diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index 83f798d44..37ab0fe91 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -567,6 +567,7 @@ bool ResolveUsernameOrPhone( .phone = phone, .messageId = post, .storyId = storyId, + .text = params.value(u"text"_q), .repliesInfo = commentId ? Window::RepliesByLinkInfo{ Window::CommentId{ commentId } @@ -901,6 +902,34 @@ bool ShowEditPersonalChannel( return true; } +bool ShowCollectiblePhone( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto phone = match->captured(1); + const auto peerId = PeerId(match->captured(2).toULongLong()); + controller->resolveCollectible( + peerId, + phone.startsWith('+') ? phone : '+' + phone); + return true; +} + +bool ShowCollectibleUsername( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto username = match->captured(1); + const auto peerId = PeerId(match->captured(2).toULongLong()); + controller->resolveCollectible(peerId, username); + return true; +} + void ExportTestChatTheme( not_null controller, not_null theme) { @@ -1311,6 +1340,14 @@ const std::vector &InternalUrlHandlers() { u"^edit_personal_channel$"_q, ShowEditPersonalChannel, }, + { + u"^collectible_phone/([\\+0-9\\-\\s]+)@([0-9]+)$"_q, + ShowCollectiblePhone, + }, + { + u"^collectible_username/([a-zA-Z0-9\\-\\_\\.]+)@([0-9]+)$"_q, + ShowCollectibleUsername, + }, }; return Result; } diff --git a/Telegram/SourceFiles/core/sandbox.cpp b/Telegram/SourceFiles/core/sandbox.cpp index fb145f7f1..6feacdb5b 100644 --- a/Telegram/SourceFiles/core/sandbox.cpp +++ b/Telegram/SourceFiles/core/sandbox.cpp @@ -33,7 +33,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include #include -#include namespace Core { namespace { @@ -518,10 +517,8 @@ void Sandbox::refreshGlobalProxy() { || proxy.type == MTP::ProxyData::Type::Http) { QNetworkProxy::setApplicationProxy( MTP::ToNetworkProxy(MTP::ToDirectIpProxy(proxy))); - } else if ((!Core::IsAppLaunched() - || Core::App().settings().proxy().isSystem()) - // this works stable only in sandboxed environment where it works through portal - && (!Platform::IsLinux() || KSandbox::isInside() || cDebugMode())) { + } else if (!Core::IsAppLaunched() + || Core::App().settings().proxy().isSystem()) { QNetworkProxyFactory::setUseSystemConfiguration(true); } else { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); diff --git a/Telegram/SourceFiles/core/ui_integration.cpp b/Telegram/SourceFiles/core/ui_integration.cpp index 01763e1e2..5f0c200a7 100644 --- a/Telegram/SourceFiles/core/ui_integration.cpp +++ b/Telegram/SourceFiles/core/ui_integration.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/stickers/data_custom_emoji.h" #include "data/data_session.h" #include "data/data_sponsored_messages.h" +#include "iv/iv_instance.h" #include "ui/text/text_custom_emoji.h" #include "ui/basic_click_handlers.h" #include "ui/emoji_config.h" @@ -241,6 +242,13 @@ bool UiIntegration::handleUrlClick( } else if (local.startsWith(u"internal:"_q, Qt::CaseInsensitive)) { Core::App().openInternalUrl(local, context); return true; + } else if (Iv::PreferForUri(url) + && !context.value().ignoreIv) { + const auto my = context.value(); + if (const auto controller = my.sessionWindow.get()) { + Core::App().iv().openWithIvPreferred(controller, url, context); + return true; + } } if (AyuUrlHandlers::TryHandleSpotify(url)) { diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index a0582bc4b..ffbc3b76c 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D666}"_cs; constexpr auto AppNameOld = "AyuGram for Windows"_cs; constexpr auto AppName = "AyuGram Desktop"_cs; constexpr auto AppFile = "AyuGram"_cs; -constexpr auto AppVersion = 4016000; -constexpr auto AppVersionStr = "4.16"; +constexpr auto AppVersion = 4016006; +constexpr auto AppVersionStr = "4.16.6"; constexpr auto AppBetaVersion = false; constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION; diff --git a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp index dd95482db..ca894acf5 100644 --- a/Telegram/SourceFiles/data/business/data_business_chatbots.cpp +++ b/Telegram/SourceFiles/data/business/data_business_chatbots.cpp @@ -125,8 +125,10 @@ void Chatbots::togglePaused(not_null peer, bool paused) { return; } else if (const auto settings = peer->barSettings()) { peer->setBarSettings(paused - ? (*settings | PeerBarSetting::BusinessBotPaused) - : (*settings & ~PeerBarSetting::BusinessBotPaused)); + ? ((*settings | PeerBarSetting::BusinessBotPaused) + & ~PeerBarSetting::BusinessBotCanReply) + : ((*settings & ~PeerBarSetting::BusinessBotPaused) + | PeerBarSetting::BusinessBotCanReply)); } else { api->requestPeerSettings(peer); } diff --git a/Telegram/SourceFiles/data/business/data_business_common.cpp b/Telegram/SourceFiles/data/business/data_business_common.cpp index 52c545bc4..149a3ee13 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.cpp +++ b/Telegram/SourceFiles/data/business/data_business_common.cpp @@ -71,6 +71,13 @@ auto RecipientsFlags(const BusinessRecipients &data) { } // namespace +BusinessRecipients BusinessRecipients::MakeValid(BusinessRecipients value) { + if (value.included.empty()) { + value.allButExcluded = true; + } + return value; +} + MTPInputBusinessRecipients ForMessagesToMTP(const BusinessRecipients &data) { using Flag = MTPDinputBusinessRecipients::Flag; const auto &chats = data.allButExcluded ? data.excluded : data.included; diff --git a/Telegram/SourceFiles/data/business/data_business_common.h b/Telegram/SourceFiles/data/business/data_business_common.h index d6f4121a6..600d5ff79 100644 --- a/Telegram/SourceFiles/data/business/data_business_common.h +++ b/Telegram/SourceFiles/data/business/data_business_common.h @@ -44,6 +44,9 @@ struct BusinessRecipients { BusinessChats excluded; bool allButExcluded = false; + [[nodiscard]] static BusinessRecipients MakeValid( + BusinessRecipients value); + friend inline bool operator==( const BusinessRecipients &a, const BusinessRecipients &b) = default; diff --git a/Telegram/SourceFiles/data/data_birthday.cpp b/Telegram/SourceFiles/data/data_birthday.cpp index 65707664f..34a2b0737 100644 --- a/Telegram/SourceFiles/data/data_birthday.cpp +++ b/Telegram/SourceFiles/data/data_birthday.cpp @@ -127,4 +127,3 @@ rpl::producer IsBirthdayTodayValue(Birthday date) { } } // namespace Data - diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index b32c85c53..b088be2d7 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -139,6 +139,10 @@ const std::vector &ChannelData::usernames() const { return _username.usernames(); } +bool ChannelData::isUsernameEditable(QString username) const { + return _username.isEditable(username); +} + void ChannelData::setAccessHash(uint64 accessHash) { access = accessHash; input = MTP_inputPeerChannel( diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index d22352020..6dbe84329 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -182,6 +182,7 @@ public: [[nodiscard]] QString username() const; [[nodiscard]] QString editableUsername() const; [[nodiscard]] const std::vector &usernames() const; + [[nodiscard]] bool isUsernameEditable(QString username) const; [[nodiscard]] int membersCount() const { return std::max(_membersCount, 1); diff --git a/Telegram/SourceFiles/data/data_download_manager.cpp b/Telegram/SourceFiles/data/data_download_manager.cpp index 95c78092d..2fa5f05d4 100644 --- a/Telegram/SourceFiles/data/data_download_manager.cpp +++ b/Telegram/SourceFiles/data/data_download_manager.cpp @@ -1123,13 +1123,14 @@ rpl::producer MakeDownloadBarContent() { state->thumbnail = Images::Prepare(embed->original(), 0, { .options = Images::Option::Blur, }); + } else if (!state->downloadTaskLifetime) { + state->document->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return self(self); + }) | rpl::start_with_next( + state->push, + state->downloadTaskLifetime); } - state->document->session().downloaderTaskFinished( - ) | rpl::filter([=] { - return self(self); - }) | rpl::start_with_next( - state->push, - state->downloadTaskLifetime); return !state->thumbnail.isNull(); }; const auto resolveThumbnail = [=] { diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 76389df4e..9b5d6d379 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -940,7 +940,7 @@ const QString &PeerData::shortName() const { return _name; } -QString PeerData::userName() const { +QString PeerData::username() const { if (const auto user = asUser()) { return user->username(); } else if (const auto channel = asChannel()) { @@ -949,6 +949,34 @@ QString PeerData::userName() const { return QString(); } +QString PeerData::editableUsername() const { + if (const auto user = asUser()) { + return user->editableUsername(); + } else if (const auto channel = asChannel()) { + return channel->editableUsername(); + } + return QString(); +} + +const std::vector &PeerData::usernames() const { + if (const auto user = asUser()) { + return user->usernames(); + } else if (const auto channel = asChannel()) { + return channel->usernames(); + } + static const auto kEmpty = std::vector(); + return kEmpty; +} + +bool PeerData::isUsernameEditable(QString username) const { + if (const auto user = asUser()) { + return user->isUsernameEditable(username); + } else if (const auto channel = asChannel()) { + return channel->isUsernameEditable(username); + } + return false; +} + bool PeerData::changeColorIndex(uint8 index) { index %= Ui::kColorIndexCount; if (_colorIndexCloud && _colorIndex == index) { diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index bf7d4e025..b7f98e599 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -279,7 +279,11 @@ public: [[nodiscard]] const QString &name() const; [[nodiscard]] const QString &shortName() const; [[nodiscard]] const QString &topBarNameText() const; - [[nodiscard]] QString userName() const; + + [[nodiscard]] QString username() const; + [[nodiscard]] QString editableUsername() const; + [[nodiscard]] const std::vector &usernames() const; + [[nodiscard]] bool isUsernameEditable(QString username) const; [[nodiscard]] const base::flat_set &nameWords() const { return _nameWords; diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 9a5160ea0..e21d11a65 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -1240,7 +1240,7 @@ PeerData *Session::peerByUsername(const QString &username) const { const auto uname = username.trimmed(); for (const auto &[peerId, peer] : _peers) { if (peer->isLoaded() - && !peer->userName().compare(uname, Qt::CaseInsensitive)) { + && !peer->username().compare(uname, Qt::CaseInsensitive)) { return peer.get(); } } diff --git a/Telegram/SourceFiles/data/data_sponsored_messages.cpp b/Telegram/SourceFiles/data/data_sponsored_messages.cpp index b36258fb8..c22cce33a 100644 --- a/Telegram/SourceFiles/data/data_sponsored_messages.cpp +++ b/Telegram/SourceFiles/data/data_sponsored_messages.cpp @@ -309,7 +309,7 @@ void SponsoredMessages::append( ? _session->data().processBotApp(peerId, *data.vapp()) : nullptr; result.botLinkInfo = Window::PeerByLinkInfo{ - .usernameOrId = user->userName(), + .usernameOrId = user->username(), .resolveType = botAppData ? Window::ResolveType::BotApp : data.vstart_param() diff --git a/Telegram/SourceFiles/data/data_story.cpp b/Telegram/SourceFiles/data/data_story.cpp index d4b12de6a..cc0fefe99 100644 --- a/Telegram/SourceFiles/data/data_story.cpp +++ b/Telegram/SourceFiles/data/data_story.cpp @@ -450,7 +450,7 @@ bool Story::hasDirectLink() const { if (!_privacyPublic || (!_pinned && expired())) { return false; } - return !_peer->userName().isEmpty(); + return !_peer->username().isEmpty(); } std::optional Story::errorTextForForward( diff --git a/Telegram/SourceFiles/data/data_user.cpp b/Telegram/SourceFiles/data/data_user.cpp index 97d480d10..c4e1bbf68 100644 --- a/Telegram/SourceFiles/data/data_user.cpp +++ b/Telegram/SourceFiles/data/data_user.cpp @@ -486,6 +486,10 @@ const std::vector &UserData::usernames() const { return _username.usernames(); } +bool UserData::isUsernameEditable(QString username) const { + return _username.isEditable(username); +} + const QString &UserData::phone() const { return _phone; } diff --git a/Telegram/SourceFiles/data/data_user.h b/Telegram/SourceFiles/data/data_user.h index 7a8a22582..67da9e3f5 100644 --- a/Telegram/SourceFiles/data/data_user.h +++ b/Telegram/SourceFiles/data/data_user.h @@ -150,15 +150,11 @@ public: // a full check by canShareThisContact() call. [[nodiscard]] bool canShareThisContactFast() const; - MTPInputUser inputUser = MTP_inputUserEmpty(); - - QString firstName; - QString lastName; [[nodiscard]] const QString &phone() const; [[nodiscard]] QString username() const; [[nodiscard]] QString editableUsername() const; [[nodiscard]] const std::vector &usernames() const; - QString nameOrPhone; + [[nodiscard]] bool isUsernameEditable(QString username) const; enum class ContactStatus : char { Unknown, @@ -186,8 +182,6 @@ public: void setBirthday(Data::Birthday value); void setBirthday(const tl::conditional &value); - std::unique_ptr botInfo; - void setUnavailableReasons( std::vector &&reasons); @@ -209,6 +203,14 @@ public: [[nodiscard]] MsgId personalChannelMessageId() const; void setPersonalChannel(ChannelId channelId, MsgId messageId); + MTPInputUser inputUser = MTP_inputUserEmpty(); + + QString firstName; + QString lastName; + QString nameOrPhone; + + std::unique_ptr botInfo; + private: auto unavailableReasons() const -> const std::vector & override; diff --git a/Telegram/SourceFiles/data/data_user_names.cpp b/Telegram/SourceFiles/data/data_user_names.cpp index b69756b5e..c2edd6b01 100644 --- a/Telegram/SourceFiles/data/data_user_names.cpp +++ b/Telegram/SourceFiles/data/data_user_names.cpp @@ -80,4 +80,10 @@ const std::vector &UsernamesInfo::usernames() const { return _usernames; } +bool UsernamesInfo::isEditable(const QString &username) const { + return (_indexEditableUsername >= 0) + && (_indexEditableUsername < _usernames.size()) + && (_usernames[_indexEditableUsername] == username); +} + } // namespace Data diff --git a/Telegram/SourceFiles/data/data_user_names.h b/Telegram/SourceFiles/data/data_user_names.h index 03853b520..04912ced9 100644 --- a/Telegram/SourceFiles/data/data_user_names.h +++ b/Telegram/SourceFiles/data/data_user_names.h @@ -27,6 +27,7 @@ public: [[nodiscard]] QString username() const; [[nodiscard]] QString editableUsername() const; [[nodiscard]] const std::vector &usernames() const; + [[nodiscard]] bool isEditable(const QString &username) const; private: std::vector _usernames; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 354a148eb..0da825209 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -1087,7 +1087,7 @@ void InnerWidget::paintPeerSearchResult( QRect tr(context.st->textLeft, context.st->textTop, namewidth, st::dialogsTextFont->height); p.setFont(st::dialogsTextFont); - QString username = peer->userName(); + QString username = peer->username(); if (!context.active && username.startsWith(_peerSearchQuery, Qt::CaseInsensitive)) { auto first = '@' + username.mid(0, _peerSearchQuery.size()); auto second = username.mid(_peerSearchQuery.size()); @@ -4021,7 +4021,7 @@ void InnerWidget::setupShortcuts() { const auto history = thread->owningHistory(); const auto isArchived = history->folder() && (history->folder()->id() == Data::Folder::kId); - + Window::ToggleHistoryArchived( _controller->uiShow(), history, diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp index ab1f19958..5899117be 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_item.cpp @@ -496,7 +496,7 @@ auto GenerateParticipantString( data, }); } - const auto username = peer->userName(); + const auto username = peer->username(); if (username.isEmpty()) { return name; } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 68407be66..ae2ba9f8a 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -4536,7 +4536,7 @@ void HistoryWidget::chooseAttach( } const auto filter = (overrideSendImagesAsPhotos == true) - ? FileDialog::ImagesOrAllFilter() + ? FileDialog::PhotoVideoFilesFilter() : FileDialog::AllOrImagesFilter(); FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( @@ -6284,8 +6284,7 @@ void HistoryWidget::startItemRevealAnimations() { void HistoryWidget::startMessageSendingAnimation( not_null item) { auto &sendingAnimation = controller()->sendingAnimation(); - if (!sendingAnimation.hasLocalMessage(item->fullId().msg) - || !sendingAnimation.checkExpectedType(item)) { + if (!sendingAnimation.checkExpectedType(item)) { return; } Assert(item->mainView() != nullptr); @@ -6297,14 +6296,16 @@ void HistoryWidget::startMessageSendingAnimation( geometryValue() | rpl::to_empty, _scroll->geometryValue() | rpl::to_empty, _list->geometryValue() | rpl::to_empty - ) | rpl::map([=] { + ) | rpl::map([=]() -> std::optional { const auto view = item->mainView(); + const auto top = view ? _list->itemTop(view) : -1; + if (top < 0) { + return std::nullopt; + } const auto additional = (_list->height() == _scroll->height()) ? view->height() : 0; - return _list->mapToGlobal(QPoint( - 0, - _list->itemTop(view) - additional)); + return _list->mapToGlobal(QPoint(0, top - additional)); }); sendingAnimation.startAnimation({ diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index c7d63eeb8..76d3e6940 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -18,7 +18,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/fields/input_field.h" #include "mtproto/sender.h" -struct FileLoadResult; enum class SendMediaType; class MessageLinksParser; struct InlineBotQuery; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index 24177943f..a05a19f48 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -1973,6 +1973,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { const auto guard = gsl::finally([&] { updateSendButtonType(); updateReplaceMediaButton(); + updateFieldPlaceholder(); updateControlsVisibility(); updateControlsGeometry(_wrap->size()); }); diff --git a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp index 858b89fbc..524719f92 100644 --- a/Telegram/SourceFiles/history/view/history_view_contact_status.cpp +++ b/Telegram/SourceFiles/history/view/history_view_contact_status.cpp @@ -902,15 +902,15 @@ void BusinessBotStatus::Bar::showState(State state) { _userpic->setAttribute(Qt::WA_TransparentForMouseEvents); _userpic->show(); _name->setText(state.bot->name()); - _status->setText(!state.canReply - ? tr::lng_chatbot_status_views(tr::now) - : state.paused + _status->setText(state.paused ? tr::lng_chatbot_status_paused(tr::now) - : tr::lng_chatbot_status_can_reply(tr::now)); + : state.canReply + ? tr::lng_chatbot_status_can_reply(tr::now) + : tr::lng_chatbot_status_views(tr::now)); _togglePaused->setText(state.paused ? tr::lng_chatbot_button_resume() : tr::lng_chatbot_button_pause()); - _togglePaused->setVisible(state.canReply); + _togglePaused->setVisible(state.canReply || state.paused); _paused = state.paused; resizeToWidth(width()); } diff --git a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp index d6e606db7..ac95a45a3 100644 --- a/Telegram/SourceFiles/history/view/history_view_list_widget.cpp +++ b/Telegram/SourceFiles/history/view/history_view_list_widget.cpp @@ -1839,16 +1839,18 @@ void ListWidget::startItemRevealAnimations() { void ListWidget::startMessageSendingAnimation( not_null item) { auto &sendingAnimation = controller()->sendingAnimation(); - if (!sendingAnimation.hasLocalMessage(item->fullId().msg) - || !sendingAnimation.checkExpectedType(item)) { + if (!sendingAnimation.checkExpectedType(item)) { return; } auto globalEndTopLeft = rpl::merge( session().data().newItemAdded() | rpl::to_empty, geometryValue() | rpl::to_empty - ) | rpl::map([=] { + ) | rpl::map([=]() -> std::optional { const auto view = viewForItem(item); + if (!view) { + return std::nullopt; + } const auto additional = !_visibleTop ? view->height() : 0; return mapToGlobal(QPoint(0, itemTop(view) - additional)); }); diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index b7ebe009b..91183100a 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -875,7 +875,7 @@ void RepliesWidget::chooseAttach( } const auto filter = (overrideSendImagesAsPhotos == true) - ? FileDialog::ImagesOrAllFilter() + ? FileDialog::PhotoVideoFilesFilter() : FileDialog::AllOrImagesFilter(); FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( FileDialog::OpenResult &&result) { diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 86c114195..df1a88f8c 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -62,11 +62,19 @@ namespace HistoryView { ScheduledMemento::ScheduledMemento(not_null history) : _history(history) , _forumTopic(nullptr) { + const auto list = _history->owner().scheduledMessages().list(_history); + if (!list.ids.empty()) { + _list.setScrollTopState({ .item = { .fullId = list.ids.front() } }); + } } ScheduledMemento::ScheduledMemento(not_null forumTopic) : _history(forumTopic->owningHistory()) , _forumTopic(forumTopic) { + const auto list = _history->owner().scheduledMessages().list(_forumTopic); + if (!list.ids.empty()) { + _list.setScrollTopState({ .item = { .fullId = list.ids.front() } }); + } } object_ptr ScheduledMemento::createWidget( @@ -1162,9 +1170,7 @@ Context ScheduledWidget::listContext() { } bool ScheduledWidget::listScrollTo(int top, bool syntetic) { - top = (top == ScrollMax && syntetic) - ? 0 - : std::clamp(top, 0, _scroll->scrollTopMax()); + top = std::clamp(top, 0, _scroll->scrollTopMax()); if (_scroll->scrollTop() == top) { updateInnerVisibleArea(); return false; diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index 850e56bb7..43702688f 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -280,7 +280,7 @@ private: }; -class ScheduledMemento : public Window::SectionMemento { +class ScheduledMemento final : public Window::SectionMemento { public: ScheduledMemento(not_null history); ScheduledMemento(not_null forumTopic); diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp index e79fa89ce..ea470af93 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/earn_format.cpp @@ -12,6 +12,7 @@ namespace Info::ChannelEarn { using EarnInt = Data::EarnInt; constexpr auto kMinorPartLength = 9; +constexpr auto kMaxChoppedZero = kMinorPartLength - 2; constexpr auto kZero = QChar('0'); constexpr auto kDot = QChar('.'); @@ -35,7 +36,7 @@ QString MinorPart(EarnInt value) { auto ch = end - 1; auto zeroCount = 0; while (ch != begin) { - if ((*ch) == kZero) { + if (((*ch) == kZero) && (zeroCount < kMaxChoppedZero)) { zeroCount++; } else { break; diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp b/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp index 1a0cedb1b..80e377e1e 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.cpp @@ -77,7 +77,7 @@ void ShowMenu(not_null box, const QString &text) { [[nodiscard]] ClickHandlerPtr LearnMoreCurrencyLink( not_null controller, not_null box) { - const auto url = u"https://telegram.org/blog/monetization-for-channels"_q; + const auto url = tr::lng_channel_earn_learn_coin_link(tr::now); using Resolver = HistoryView::Controls::WebpageResolver; const auto resolver = box->lifetime().make_state( @@ -217,7 +217,18 @@ void AddRecipient(not_null box, const TextWithEntities &t) { } #endif -[[nodiscard]] QImage IconCurrency( +[[nodiscard]] QString FormatDate(const QDateTime &date) { + return tr::lng_group_call_starts_short_date( + tr::now, + lt_date, + langDayOfMonth(date.date()), + lt_time, + QLocale().toString(date.time(), QLocale::ShortFormat)); +} + +} // namespace + +QImage IconCurrency( const style::FlatLabel &label, const QColor &c) { const auto s = Size(label.style.font->ascent); @@ -234,17 +245,6 @@ void AddRecipient(not_null box, const TextWithEntities &t) { return image; } -[[nodiscard]] QString FormatDate(const QDateTime &date) { - return tr::lng_group_call_starts_short_date( - tr::now, - lt_date, - langDayOfMonth(date.date()), - lt_time, - QLocale().toString(date.time(), QLocale::ShortFormat)); -} - -} // namespace - InnerWidget::InnerWidget( QWidget *parent, not_null controller, @@ -372,6 +372,7 @@ void InnerWidget::fill() { widget->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(widget); + auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::activeButtonBg); p.drawEllipse(rect); diff --git a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.h b/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.h index 69d1587d8..99e9511be 100644 --- a/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.h +++ b/Telegram/SourceFiles/info/channel_statistics/earn/info_earn_inner_widget.h @@ -23,6 +23,10 @@ namespace Info::ChannelEarn { class Memento; +[[nodiscard]] QImage IconCurrency( + const style::FlatLabel &label, + const QColor &c); + class InnerWidget final : public Ui::VerticalLayout { public: struct ShowRequest final { diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index df590a4bc..7da9da56b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -123,21 +123,25 @@ base::options::toggle ShowPeerIdBelowAbout({ [[nodiscard]] Fn UsernamesLinkCallback( not_null peer, - std::shared_ptr show, + not_null controller, const QString &addToLink) { + const auto weak = base::make_weak(controller); return [=](QString link) { - auto settings = &AyuSettings::getInstance(); - if (!settings->copyUsernameAsLink) { - link = '@' + link.mid(13); - } else { - if (!link.startsWith(u"https://"_q)) { - link = peer->session().createInternalLinkFull(peer->userName()) - + addToLink; - } + if (link.startsWith(u"internal:"_q)) { + Core::App().openInternalUrl(link, + QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = weak, + })); + return; + } else if (!link.startsWith(u"https://"_q)) { + link = peer->session().createInternalLinkFull(peer->username()) + + addToLink; } if (!link.isEmpty()) { QGuiApplication::clipboard()->setText(link); - show->showToast(tr::lng_username_copied(tr::now)); + if (const auto window = weak.get()) { + window->showToast(tr::lng_username_copied(tr::now)); + } } }; } @@ -1055,16 +1059,13 @@ object_ptr DetailsFiller::setupInfo() { UsernameValue(user, true) | rpl::map([=](TextWithEntities u) { return u.text.isEmpty() ? TextWithEntities() - : Ui::Text::Link( - u, - user->session().createInternalLinkFull( - u.text.mid(1))); + : Ui::Text::Link(u, UsernameUrl(user, u.text.mid(1))); }), QString(), st::infoProfileLabeledUsernamePadding); const auto callback = UsernamesLinkCallback( _peer, - controller->uiShow(), + controller, QString()); const auto hook = [=](Ui::FlatLabel::ContextMenuRequest request) { if (!request.link) { @@ -1108,7 +1109,7 @@ object_ptr DetailsFiller::setupInfo() { }, copyUsername->lifetime()); copyUsername->setClickedCallback([=] { const auto link = user->session().createInternalLinkFull( - user->userName()); + user->username()); if (!link.isEmpty()) { QGuiApplication::clipboard()->setText(link); controller->showToast(tr::lng_username_copied(tr::now)); @@ -1180,14 +1181,15 @@ object_ptr DetailsFiller::setupInfo() { auto linkText = LinkValue( _peer, true - ) | rpl::map([=](const QString &link) { - return link.isEmpty() + ) | rpl::map([=](const LinkWithUrl &link) { + const auto text = link.text; + return text.isEmpty() ? TextWithEntities() : Ui::Text::Link( - (link.startsWith(u"https://"_q) - ? link.mid(u"https://"_q.size()) - : link) + addToLink, - link + addToLink); + (text.startsWith(u"https://"_q) + ? text.mid(u"https://"_q.size()) + : text) + addToLink, + (addToLink.isEmpty() ? link.url : (text + addToLink))); }); auto linkLine = addInfoOneLine( (topicRootId @@ -1198,7 +1200,7 @@ object_ptr DetailsFiller::setupInfo() { const auto controller = _controller->parentController(); const auto linkCallback = UsernamesLinkCallback( _peer, - controller->uiShow(), + controller, addToLink); linkLine.text->overrideLinkClickHandler(linkCallback); linkLine.subtext->overrideLinkClickHandler(linkCallback); diff --git a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp index 58c51ed31..85fde827f 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp @@ -112,23 +112,22 @@ int TextItem::contentHeight() const { } // namespace -void AddPhoneMenu(not_null menu, not_null user) { - if (user->isSelf()) { - return; - } +bool IsCollectiblePhone(not_null user) { using Strings = std::vector; const auto prefixes = user->session().appConfig().get( u"fragment_prefixes"_q, - std::vector()); - { - const auto proj = [&phone = user->phone()](const QString &p) { - return phone.startsWith(p); - }; - if (ranges::none_of(prefixes, proj)) { - return; - } - } - if (const auto url = AppConfig::FragmentLink(&user->session())) { + Strings{ u"888"_q }); + const auto phone = user->phone(); + const auto proj = [&](const QString &p) { + return phone.startsWith(p); + }; + return ranges::any_of(prefixes, proj); +} + +void AddPhoneMenu(not_null menu, not_null user) { + if (user->isSelf() || !IsCollectiblePhone(user)) { + return; + } else if (const auto url = AppConfig::FragmentLink(&user->session())) { menu->addSeparator(&st::expandedMenuSeparator); const auto link = Ui::Text::Link( tr::lng_info_mobile_context_menu_fragment_about_link(tr::now), diff --git a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.h b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.h index 82ed6bb8c..25e6c8b5c 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_phone_menu.h +++ b/Telegram/SourceFiles/info/profile/info_profile_phone_menu.h @@ -16,6 +16,8 @@ class PopupMenu; namespace Info { namespace Profile { +[[nodiscard]] bool IsCollectiblePhone(not_null user); + void AddPhoneMenu(not_null menu, not_null user); } // namespace Profile diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index 9fe23da1f..3a0f9566e 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_participants.h" #include "apiwrap.h" +#include "info/profile/info_profile_phone_menu.h" #include "info/profile/info_profile_badge.h" #include "core/application.h" #include "core/click_handler_types.h" @@ -55,7 +56,7 @@ auto PlainUsernameValue(not_null peer) { peer->session().changes().peerFlagsValue(peer, UpdateFlag::Username), peer->session().changes().peerFlagsValue(peer, UpdateFlag::Usernames) ) | rpl::map([=] { - return peer->userName(); + return peer->username(); }); } @@ -136,14 +137,19 @@ rpl::producer PhoneOrHiddenValue(not_null user) { PlainUsernameValue(user), PlainAboutValue(user), tr::lng_info_mobile_hidden() - ) | rpl::map([]( + ) | rpl::map([user]( const TextWithEntities &phone, const QString &username, const QString &about, const QString &hidden) { - return (phone.text.isEmpty() && username.isEmpty() && about.isEmpty()) - ? Ui::Text::WithEntities(hidden) - : phone; + if (phone.text.isEmpty() && username.isEmpty() && about.isEmpty()) { + return Ui::Text::WithEntities(hidden); + } else if (IsCollectiblePhone(user)) { + return Ui::Text::Link(phone, u"internal:collectible_phone/"_q + + user->phone() + '@' + QString::number(user->id.value)); + } else { + return phone; + } }); } @@ -160,15 +166,22 @@ rpl::producer UsernameValue( }) | Ui::Text::ToWithEntities(); } +QString UsernameUrl(not_null peer, const QString &username) { + return peer->isUsernameEditable(username) + ? peer->session().createInternalLinkFull(username) + : (u"internal:collectible_username/"_q + + username + + "@" + + QString::number(peer->id.value)); +} + rpl::producer> UsernamesValue( not_null peer) { const auto map = [=](const std::vector &usernames) { return ranges::views::all( usernames ) | ranges::views::transform([&](const QString &u) { - return Ui::Text::Link( - u, - peer->session().createInternalLinkFull(u)); + return Ui::Text::Link(u, UsernameUrl(peer, u)); }) | ranges::to_vector; }; auto value = rpl::merge( @@ -219,14 +232,19 @@ rpl::producer AboutValue(not_null peer) { }); } -rpl::producer LinkValue(not_null peer, bool primary) { +rpl::producer LinkValue(not_null peer, bool primary) { return (primary ? PlainPrimaryUsernameValue(peer) : PlainUsernameValue(peer) | rpl::type_erased() ) | rpl::map([=](QString &&username) { - return username.isEmpty() - ? QString() - : peer->session().createInternalLinkFull(username); + return LinkWithUrl{ + .text = (username.isEmpty() + ? QString() + : peer->session().createInternalLinkFull(username)), + .url = (username.isEmpty() + ? QString() + : UsernameUrl(peer, username)), + }; }); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.h b/Telegram/SourceFiles/info/profile/info_profile_values.h index 6b7ccf475..72d50055a 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.h +++ b/Telegram/SourceFiles/info/profile/info_profile_values.h @@ -61,14 +61,23 @@ rpl::producer> MigratedOrMeValue( bool primary = false); [[nodiscard]] rpl::producer> UsernamesValue( not_null peer); +[[nodiscard]] QString UsernameUrl( + not_null peer, + const QString &username); [[nodiscard]] TextWithEntities AboutWithEntities( not_null peer, const QString &value); [[nodiscard]] rpl::producer AboutValue( not_null peer); -[[nodiscard]] rpl::producer LinkValue( + +struct LinkWithUrl { + QString text; + QString url; +}; +[[nodiscard]] rpl::producer LinkValue( not_null peer, bool primary = false); + [[nodiscard]] rpl::producer LocationValue( not_null channel); [[nodiscard]] rpl::producer NotificationsEnabledValue( diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 056aa224e..682afe3de 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -17,10 +17,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_session.h" +#include "data/data_web_page.h" #include "main/main_session.h" #include "main/main_domain.h" #include "storage/storage_domain.h" #include "info/profile/info_profile_values.h" +#include "iv/iv_instance.h" #include "ui/boxes/confirm_box.h" #include "ui/chat/attach/attach_bot_webview.h" #include "ui/widgets/checkbox.h" @@ -653,6 +655,15 @@ void AttachWebView::botHandleMenuButton(Ui::BotWebView::MenuButton button) { } } +void AttachWebView::botOpenIvLink(QString uri) { + const auto window = _context ? _context->controller.get() : nullptr; + if (window) { + Core::App().iv().openWithIvPreferred(window, uri); + } else { + Core::App().iv().openWithIvPreferred(_session, uri); + } +} + void AttachWebView::botSendData(QByteArray data) { if (!_context || _context->fromSwitch diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index d1d2c0e44..aebf99332 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -165,6 +165,7 @@ private: bool botHandleLocalUri(QString uri, bool keepOpen) override; void botHandleInvoice(QString slug) override; void botHandleMenuButton(Ui::BotWebView::MenuButton button) override; + void botOpenIvLink(QString uri) override; void botSendData(QByteArray data) override; void botSwitchInlineQuery( std::vector chatTypes, diff --git a/Telegram/SourceFiles/iv/iv_data.cpp b/Telegram/SourceFiles/iv/iv_data.cpp index bbcca0b2c..5258e6c9f 100644 --- a/Telegram/SourceFiles/iv/iv_data.cpp +++ b/Telegram/SourceFiles/iv/iv_data.cpp @@ -25,7 +25,7 @@ QByteArray GeoPointId(Geo point) { const auto lon = int(point.lon * 1000000); const auto combined = (std::uint64_t(std::uint32_t(lat)) << 32) | std::uint64_t(std::uint32_t(lon)); - return QByteArray::number(combined) + return QByteArray::number(quint64(combined)) + ',' + QByteArray::number(point.access); } diff --git a/Telegram/SourceFiles/iv/iv_instance.cpp b/Telegram/SourceFiles/iv/iv_instance.cpp index 517415435..2362a6b61 100644 --- a/Telegram/SourceFiles/iv/iv_instance.cpp +++ b/Telegram/SourceFiles/iv/iv_instance.cpp @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #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" @@ -63,7 +64,7 @@ class Shown final : public base::has_weak_ptr { public: Shown( not_null delegate, - std::shared_ptr show, + not_null session, not_null data, QString hash); @@ -169,12 +170,11 @@ private: Shown::Shown( not_null delegate, - std::shared_ptr show, + not_null session, not_null data, QString hash) : _delegate(delegate) -, _session(&show->session()) -, _show(show) { +, _session(session) { prepare(data, hash); } @@ -818,7 +818,13 @@ void Instance::show( std::shared_ptr show, not_null data, QString hash) { - const auto session = &show->session(); + this->show(&show->session(), data, hash); +} + +void Instance::show( + not_null session, + not_null data, + QString hash) { const auto guard = gsl::finally([&] { if (data->partial()) { requestFull(session, data->id()); @@ -828,10 +834,13 @@ void Instance::show( _shown->moveTo(data, hash); return; } - _shown = std::make_unique(_delegate, show, data, hash); + _shown = std::make_unique(_delegate, session, data, hash); _shownSession = session; _shown->events() | rpl::start_with_next([=](Controller::Event event) { using Type = Controller::Event::Type; + const auto lower = event.url.toLower(); + const auto urlChecked = lower.startsWith("http://") + || lower.startsWith("https://"); switch (event.type) { case Type::Close: _shown = nullptr; @@ -846,7 +855,9 @@ void Instance::show( processJoinChannel(event.context); break; case Type::OpenLinkExternal: - File::OpenUrl(event.url); + if (urlChecked) { + File::OpenUrl(event.url); + } closeAll(); break; case Type::OpenMedia: @@ -885,6 +896,9 @@ void Instance::show( break; case Type::OpenPage: case Type::OpenLink: + if (!urlChecked) { + break; + } _shownSession->api().request(MTPmessages_GetWebPage( MTP_string(event.url), MTP_int(0) @@ -896,7 +910,7 @@ void Instance::show( if (page && page->iv) { const auto parts = event.url.split('#'); const auto hash = (parts.size() > 1) ? parts[1] : u""_q; - this->show(show, page->iv.get(), hash); + this->show(_shownSession, page->iv.get(), hash); } else { UrlClickHandler::Open(event.url); } @@ -921,20 +935,104 @@ void Instance::show( } }, _shown->lifetime()); - if (!_tracking.contains(session)) { - _tracking.emplace(session); - session->lifetime().add([=] { - _tracking.remove(session); - _joining.remove(session); - _fullRequested.remove(session); - if (_shownSession == session) { - _shownSession = nullptr; - } - if (_shown && _shown->showingFrom(session)) { - _shown = nullptr; - } - }); + 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; + UrlClickHandler::Open(uri, QVariant::fromValue(my)); + }; + 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; + _ivRequestId = session->api().request(MTPmessages_GetWebPage( + MTP_string(url), + MTP_int(0) + )).done([=](const MTPmessages_WebPage &result) { + const auto &data = result.data(); + session->data().processUsers(data.vusers()); + session->data().processChats(data.vchats()); + finish(session->data().processWebpage(data.vwebpage())); + }).fail([=] { + finish(nullptr); + }).send(); } void Instance::requestFull( @@ -1028,4 +1126,17 @@ void Instance::closeAll() { _shown = 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 diff --git a/Telegram/SourceFiles/iv/iv_instance.h b/Telegram/SourceFiles/iv/iv_instance.h index ba3d8f72c..1299bbddd 100644 --- a/Telegram/SourceFiles/iv/iv_instance.h +++ b/Telegram/SourceFiles/iv/iv_instance.h @@ -36,6 +36,19 @@ public: std::shared_ptr show, not_null data, QString hash); + void show( + not_null session, + not_null data, + QString hash); + + void openWithIvPreferred( + not_null controller, + QString uri, + QVariant context = {}); + void openWithIvPreferred( + not_null session, + QString uri, + QVariant context = {}); [[nodiscard]] bool hasActiveWindow( not_null session) const; @@ -52,6 +65,8 @@ private: void processJoinChannel(const QString &context); void requestFull(not_null session, const QString &id); + void trackSession(not_null session); + const not_null _delegate; std::unique_ptr _shown; @@ -64,8 +79,18 @@ private: not_null, base::flat_set> _fullRequested; + base::flat_map< + not_null, + base::flat_map> _ivCache; + Main::Session *_ivRequestSession = nullptr; + QString _ivRequestUri; + mtpRequestId _ivRequestId = 0; + + rpl::lifetime _lifetime; }; +[[nodiscard]] bool PreferForUri(const QString &uri); + } // namespace Iv diff --git a/Telegram/SourceFiles/iv/iv_prepare.cpp b/Telegram/SourceFiles/iv/iv_prepare.cpp index ff21e4e55..6069d4407 100644 --- a/Telegram/SourceFiles/iv/iv_prepare.cpp +++ b/Telegram/SourceFiles/iv/iv_prepare.cpp @@ -51,7 +51,7 @@ template >> return Number(base::SafeRound(value * 10000.) / 100.); }; -[[nodiscard]] QByteArray EscapeAttr(QByteArray value) { +[[nodiscard]] QByteArray Escape(QByteArray value) { auto result = QByteArray(); result.reserve(value.size()); for (const auto &ch : value) { @@ -67,8 +67,8 @@ template >> return result; } -[[nodiscard]] QByteArray EscapeText(QByteArray value) { - return EscapeAttr(value); +[[nodiscard]] QByteArray Date(TimeId date) { + return Escape(langDateTimeFull(base::unixtime::parse(date)).toUtf8()); } class Parser final { @@ -391,9 +391,7 @@ QByteArray Parser::block(const MTPDpageBlockSubtitle &data) { QByteArray Parser::block(const MTPDpageBlockAuthorDate &data) { auto inner = rich(data.vauthor()); if (const auto date = data.vpublished_date().v) { - const auto parsed = base::unixtime::parse(date); - inner += " \xE2\x80\xA2 " - + tag("time", EscapeText(langDateTimeFull(parsed).toUtf8())); + inner += " \xE2\x80\xA2 " + tag("time", Date(date)); } return tag("address", inner); } @@ -412,7 +410,7 @@ QByteArray Parser::block(const MTPDpageBlockParagraph &data) { QByteArray Parser::block(const MTPDpageBlockPreformatted &data) { auto list = Attributes(); - const auto language = EscapeAttr(utf(data.vlanguage())); + const auto language = utf(data.vlanguage()); if (!language.isEmpty()) { list.push_back({ "data-language", language }); list.push_back({ "class", "lang-" + language }); @@ -430,7 +428,7 @@ QByteArray Parser::block(const MTPDpageBlockDivider &data) { } QByteArray Parser::block(const MTPDpageBlockAnchor &data) { - return tag("a", { { "name", EscapeAttr(utf(data.vname())) } }); + return tag("a", { { "name", utf(data.vname()) } }); } QByteArray Parser::block(const MTPDpageBlockList &data) { @@ -507,15 +505,13 @@ QByteArray Parser::block( }; auto result = tag("div", attributes, inner); - const auto href = data.vurl() - ? utf(*data.vurl()) - : photoFullUrl(photo); + const auto href = data.vurl() ? utf(*data.vurl()) : photoFullUrl(photo); const auto id = Number(photo.id); result = tag("a", { { "href", href }, { "oncontextmenu", data.vurl() ? QByteArray() : "return false;" }, { "data-context", data.vurl() ? QByteArray() : "viewer-photo" + id }, - }, result); + }, result); if (!slideshow) { result += caption(data.vcaption()); } @@ -621,9 +617,9 @@ QByteArray Parser::block(const MTPDpageBlockEmbed &data) { attributes.push_back({ "height", iframeHeight }); if (const auto url = data.vurl()) { if (!autosize) { - attributes.push_back({ "src", EscapeAttr(utf(url)) }); + attributes.push_back({ "src", utf(url) }); } else { - attributes.push_back({ "srcdoc", EscapeAttr(utf(url)) }); + attributes.push_back({ "srcdoc", utf(url) }); } } else if (const auto html = data.vhtml()) { attributes.push_back({ "src", embedUrl(html->v) }); @@ -661,12 +657,10 @@ QByteArray Parser::block(const MTPDpageBlockEmbedPost &data) { address += tag( "a", { { "rel", "author" }, { "onclick", "return false;" } }, - EscapeText(utf(data.vauthor()))); + utf(data.vauthor())); if (const auto date = data.vdate().v) { const auto parsed = base::unixtime::parse(date); - address += tag( - "time", - EscapeText(langDateTimeFull(parsed).toUtf8())); + address += tag("time", Date(date)); } const auto inner = tag("address", address) + list(data.vblocks()); result = tag("blockquote", { { "class", "embed-post" } }, inner); @@ -675,7 +669,7 @@ QByteArray Parser::block(const MTPDpageBlockEmbedPost &data) { const auto inner = tag("strong", utf(data.vauthor())) + tag( "small", - tag("a", { { "href", EscapeAttr(url) } }, EscapeText(url))); + tag("a", { { "href", url } }, url)); result = tag("section", { { "class", "embed-post" } }, inner); } result += caption(data.vcaption()); @@ -732,7 +726,7 @@ QByteArray Parser::block(const MTPDpageBlockChannel &data) { : "https://t.me/" + username; result = tag( "a", - { { "href", EscapeAttr(link) }, { "data-context", "channel" + id } }, + { { "href", link }, { "data-context", "channel" + id } }, result); _result.channelIds.emplace(id); return tag("section", { @@ -839,23 +833,21 @@ QByteArray Parser::block(const MTPDpageRelatedArticle &data) { inner += tag( "span", { { "class", "related-link-title" } }, - EscapeText(utf(*title))); + utf(*title)); } if (description) { inner += tag( "span", { { "class", "related-link-desc" } }, - EscapeText(utf(*description))); + utf(*description)); } if (author || published) { - const auto separator = (author && published) ? ", " : ""; - const auto parsed = base::unixtime::parse(published->v); inner += tag( "span", { { "class", "related-link-source" } }, - EscapeText((author ? utf(*author) : QByteArray()) - + separator - + langDateTimeFull(parsed).toUtf8())); + ((author ? utf(*author) : QByteArray()) + + ((author && published) ? ", " : QByteArray()) + + (published ? Date(published->v) : QByteArray()))); } result += tag("span", { { "class", "related-link-content" }, @@ -912,22 +904,25 @@ QByteArray Parser::block(const MTPDpageListItemBlocks &data) { } QByteArray Parser::block(const MTPDpageListOrderedItemText &data) { - return tag("li", { { "value", utf(data.vnum()) } }, rich(data.vtext())); + return tag( + "li", + { { "value", utf(data.vnum()) } }, + rich(data.vtext())); } QByteArray Parser::block(const MTPDpageListOrderedItemBlocks &data) { return tag( "li", - { { "value", EscapeAttr(utf(data.vnum())) } }, + { { "value", utf(data.vnum()) } }, list(data.vblocks())); } QByteArray Parser::utf(const MTPstring &text) { - return text.v; + return Escape(text.v); } QByteArray Parser::utf(const tl::conditional &text) { - return text ? text->v : QByteArray(); + return text ? utf(*text) : QByteArray(); } QByteArray Parser::tag( @@ -965,7 +960,7 @@ QByteArray Parser::rich(const MTPRichText &text) { { "\xE2\x81\xA8", "" }, { "\xE2\x81\xA9", "" }, }; - auto text = EscapeText(utf(data.vtext())); + auto text = utf(data.vtext()); for (const auto &[from, to] : replacements) { text.replace(from, to); } @@ -1016,8 +1011,8 @@ QByteArray Parser::rich(const MTPRichText &text) { }, rich(data.vtext())); }, [&](const MTPDtextEmail &data) { return tag("a", { - { "href", "mailto:" + EscapeAttr(utf(data.vemail())) }, - }, rich(data.vtext())); + { "href", "mailto:" + utf(data.vemail()) }, + }, rich(data.vtext())); }, [&](const MTPDtextSubscript &data) { return tag("sub", rich(data.vtext())); }, [&](const MTPDtextSuperscript &data) { @@ -1026,11 +1021,11 @@ QByteArray Parser::rich(const MTPRichText &text) { return tag("mark", rich(data.vtext())); }, [&](const MTPDtextPhone &data) { return tag("a", { - { "href", "tel:" + EscapeAttr(utf(data.vphone())) }, + { "href", "tel:" + utf(data.vphone()) }, }, rich(data.vtext())); }, [&](const MTPDtextAnchor &data) { const auto inner = rich(data.vtext()); - const auto name = EscapeAttr(utf(data.vname())); + const auto name = utf(data.vname()); return inner.isEmpty() ? tag("a", { { "name", name } }) : tag( diff --git a/Telegram/SourceFiles/main/main_domain.cpp b/Telegram/SourceFiles/main/main_domain.cpp index 26e01229f..aafbcd2ac 100644 --- a/Telegram/SourceFiles/main/main_domain.cpp +++ b/Telegram/SourceFiles/main/main_domain.cpp @@ -32,11 +32,13 @@ Domain::Domain(const QString &dataName) : _dataName(dataName) , _local(std::make_unique(this, dataName)) { _active.changes( - ) | rpl::take(1) | rpl::start_with_next([] { + ) | rpl::take(1) | rpl::start_with_next([=] { // In case we had a legacy passcoded app we start settings here. Core::App().startSettingsAndBackground(); - Core::App().notifications().createManager(); + crl::on_main(this, [=] { + Core::App().notifications().createManager(); + }); }, _lifetime); _active.changes( @@ -51,7 +53,7 @@ Domain::Domain(const QString &dataName) : rpl::never(); }) | rpl::flatten_latest( ) | rpl::start_with_next([](const Data::PeerUpdate &update) { - CrashReports::SetAnnotation("Username", update.peer->userName()); + CrashReports::SetAnnotation("Username", update.peer->username()); }, _lifetime); } diff --git a/Telegram/SourceFiles/main/main_domain.h b/Telegram/SourceFiles/main/main_domain.h index 00ed7570e..8379a629a 100644 --- a/Telegram/SourceFiles/main/main_domain.h +++ b/Telegram/SourceFiles/main/main_domain.h @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/timer.h" +#include "base/weak_ptr.h" namespace Storage { class Domain; @@ -23,7 +24,7 @@ namespace Main { class Account; class Session; -class Domain final { +class Domain final : public base::has_weak_ptr { public: struct AccountWithIndex { int index = 0; diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index c2ddd0d39..dba3ea8e6 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -16,7 +16,6 @@ struct HistoryMessageMarkupButton; class MainWindow; class HistoryWidget; class StackItem; -struct FileLoadResult; class History; class Image; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 62404da81..a61a1c295 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -933,6 +933,13 @@ void Controller::show( peer->updateFull(); } +void Controller::jumpTo( + not_null story, + Data::StoriesContext context) { + show(story, std::move(context)); + _delegate->storiesRedisplay(story); +} + bool Controller::changeShown(Data::Story *story) { const auto id = story ? story->fullId() : FullStoryId(); const auto session = story ? &story->session() : nullptr; diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index b8ad1e20b..b8745d4ed 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -141,6 +141,7 @@ public: -> HistoryView::Reactions::CachedIconFactory &; void show(not_null story, Data::StoriesContext context); + void jumpTo(not_null story, Data::StoriesContext context); void ready(); void updateVideoPlayback(const Player::TrackState &state); diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index d6959d1a6..b1a33d9c1 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -431,7 +431,7 @@ void ReplyArea::chooseAttach( } const auto filter = (overrideSendImagesAsPhotos == true) - ? FileDialog::ImagesOrAllFilter() + ? FileDialog::PhotoVideoFilesFilter() : FileDialog::AllOrImagesFilter(); const auto weak = make_weak(&_shownPeerGuard); const auto callback = [=](FileDialog::OpenResult &&result) { diff --git a/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp index 608d7e6ab..05d7b588b 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_repost_view.cpp @@ -184,7 +184,7 @@ RepostClickHandler RepostView::lookupHandler(QPoint position) { const auto of = owner->stories().lookup({ peer->id, id }); if (of) { using namespace Data; - _controller->show(*of, { StoriesContextSingle() }); + _controller->jumpTo(*of, { StoriesContextSingle() }); } else { _controller->uiShow()->show(PrepareShortInfoBox(peer)); } diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 60c018a10..6d3ccf593 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -3262,7 +3262,7 @@ not_null OverlayWidget::widget() const { void OverlayWidget::hide() { clearBeforeHide(); - //applyHideWindowWorkaround(); + applyHideWindowWorkaround(); _window->hide(); } diff --git a/Telegram/SourceFiles/menu/menu_sponsored.cpp b/Telegram/SourceFiles/menu/menu_sponsored.cpp index bb8109d2b..bfa8427f1 100644 --- a/Telegram/SourceFiles/menu/menu_sponsored.cpp +++ b/Telegram/SourceFiles/menu/menu_sponsored.cpp @@ -60,6 +60,7 @@ void AboutBox( widget->paintRequest( ) | rpl::start_with_next([=] { auto p = Painter(widget); + auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::activeButtonBg); p.drawEllipse(rect); diff --git a/Telegram/SourceFiles/mtproto/facade.h b/Telegram/SourceFiles/mtproto/facade.h index 989fdad4b..39e2f732c 100644 --- a/Telegram/SourceFiles/mtproto/facade.h +++ b/Telegram/SourceFiles/mtproto/facade.h @@ -40,10 +40,6 @@ constexpr ShiftedDcId groupCallStreamDcId(DcId dcId) { return ShiftDcId(dcId, kGroupCallStreamDcShift); } -constexpr auto kUploadSessionsCount = 2; - -constexpr auto kUploadSessionsCountMax = 8; - namespace details { constexpr ShiftedDcId downloadDcId(DcId dcId, int index) { @@ -94,7 +90,6 @@ inline DcId getTemporaryIdFromRealDcId(ShiftedDcId shiftedDcId) { namespace details { constexpr ShiftedDcId uploadDcId(DcId dcId, int index) { - static_assert(kUploadSessionsCountMax < kMaxMediaDcCount, "Too large MTPUploadSessionsCount!"); return ShiftDcId(dcId, kBaseUploadDcShift + index); }; @@ -103,14 +98,14 @@ constexpr ShiftedDcId uploadDcId(DcId dcId, int index) { // send(req, callbacks, MTP::uploadDcId(index)) - for upload shifted dc id // uploading always to the main dc so BareDcId(result) == 0 inline ShiftedDcId uploadDcId(int index) { - Expects(index >= 0 && index < kUploadSessionsCountMax); + Expects(index >= 0 && index < kMaxMediaDcCount); return details::uploadDcId(0, index); }; constexpr bool isUploadDcId(ShiftedDcId shiftedDcId) { return (shiftedDcId >= details::uploadDcId(0, 0)) - && (shiftedDcId < details::uploadDcId(0, kUploadSessionsCountMax - 1) + kDcShift); + && (shiftedDcId < details::uploadDcId(0, kMaxMediaDcCount - 1) + kDcShift); } inline ShiftedDcId destroyKeyNextDcId(ShiftedDcId shiftedDcId) { diff --git a/Telegram/SourceFiles/mtproto/mtp_instance.cpp b/Telegram/SourceFiles/mtproto/mtp_instance.cpp index 861d6cd44..76c5cb623 100644 --- a/Telegram/SourceFiles/mtproto/mtp_instance.cpp +++ b/Telegram/SourceFiles/mtproto/mtp_instance.cpp @@ -18,7 +18,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localstorage.h" #include "calls/calls_instance.h" #include "main/main_account.h" // Account::configUpdated. -#include "apiwrap.h" #include "core/application.h" #include "core/core_settings.h" #include "lang/lang_instance.h" diff --git a/Telegram/SourceFiles/mtproto/session_private.cpp b/Telegram/SourceFiles/mtproto/session_private.cpp index 729a95ed6..96a0c8268 100644 --- a/Telegram/SourceFiles/mtproto/session_private.cpp +++ b/Telegram/SourceFiles/mtproto/session_private.cpp @@ -60,6 +60,8 @@ constexpr auto kSendStateRequestWaiting = crl::time(1000); // How much time to wait for some more requests, when sending msg acks. constexpr auto kAckSendWaiting = 10 * crl::time(1000); +constexpr auto kCutContainerOnSize = 16 * 1024; + auto SyncTimeRequestDuration = kFastRequestDuration; using namespace details; @@ -696,7 +698,8 @@ void SessionPrivate::tryToSend() { initSize = initSizeInInts * sizeof(mtpPrime); } - bool needAnyResponse = false; + auto needAnyResponse = false; + auto someSkipped = false; SerializedRequest toSendRequest; { QWriteLocker locker1(_sessionData->toSendMutex()); @@ -711,15 +714,33 @@ void SessionPrivate::tryToSend() { locker1.unlock(); } - uint32 toSendCount = toSend.size(); - if (pingRequest) ++toSendCount; - if (ackRequest) ++toSendCount; - if (resendRequest) ++toSendCount; - if (stateRequest) ++toSendCount; - if (httpWaitRequest) ++toSendCount; - if (bindDcKeyRequest) ++toSendCount; + auto totalSending = int(toSend.size()); + auto sendingFrom = begin(toSend); + auto sendingTill = end(toSend); + auto combinedLength = 0; + for (auto i = sendingFrom; i != sendingTill; ++i) { + combinedLength += i->second->size(); + if (combinedLength >= kCutContainerOnSize) { + ++i; + if (const auto skipping = int(sendingTill - i)) { + sendingTill = i; + totalSending -= skipping; + Assert(totalSending > 0); + someSkipped = true; + } + break; + } + } + auto sendingRange = ranges::make_subrange(sendingFrom, sendingTill); + const auto sendingCount = totalSending; + if (pingRequest) ++totalSending; + if (ackRequest) ++totalSending; + if (resendRequest) ++totalSending; + if (stateRequest) ++totalSending; + if (httpWaitRequest) ++totalSending; + if (bindDcKeyRequest) ++totalSending; - if (!toSendCount) { + if (!totalSending) { return; // nothing to send } @@ -735,11 +756,11 @@ void SessionPrivate::tryToSend() { ? httpWaitRequest : bindDcKeyRequest ? bindDcKeyRequest - : toSend.begin()->second; - if (toSendCount == 1 && !first->forceSendInContainer) { + : sendingRange.begin()->second; + if (totalSending == 1 && !first->forceSendInContainer) { toSendRequest = first; if (sendAll) { - toSend.clear(); + toSend.erase(sendingFrom, sendingTill); locker1.unlock(); } @@ -808,7 +829,7 @@ void SessionPrivate::tryToSend() { if (stateRequest) containerSize += stateRequest.messageSize(); if (httpWaitRequest) containerSize += httpWaitRequest.messageSize(); if (bindDcKeyRequest) containerSize += bindDcKeyRequest.messageSize(); - for (const auto &[requestId, request] : toSend) { + for (const auto &[requestId, request] : sendingRange) { containerSize += request.messageSize(); if (needsLayer && request->needsLayer) { containerSize += initSizeInInts; @@ -825,9 +846,9 @@ void SessionPrivate::tryToSend() { // prepare container + each in invoke after toSendRequest = SerializedRequest::Prepare( containerSize, - containerSize + 3 * toSend.size()); + containerSize + 3 * sendingCount); toSendRequest->push_back(mtpc_msg_container); - toSendRequest->push_back(toSendCount); + toSendRequest->push_back(totalSending); // check for a valid container auto bigMsgId = base::unixtime::mtproto_msg_id(); @@ -839,7 +860,7 @@ void SessionPrivate::tryToSend() { // prepare sent container auto sentIdsWrap = SentContainer(); sentIdsWrap.sent = crl::now(); - sentIdsWrap.messages.reserve(toSendCount); + sentIdsWrap.messages.reserve(totalSending); if (bindDcKeyRequest) { _bindMsgId = placeToContainer( @@ -848,6 +869,7 @@ void SessionPrivate::tryToSend() { false, bindDcKeyRequest); _bindMessageSent = crl::now(); + sentIdsWrap.messages.push_back(_bindMsgId); needAnyResponse = true; } if (pingRequest) { @@ -856,10 +878,11 @@ void SessionPrivate::tryToSend() { bigMsgId, forceNewMsgId, pingRequest); + sentIdsWrap.messages.push_back(_pingMsgId); needAnyResponse = true; } - for (auto &[requestId, request] : toSend) { + for (auto &[requestId, request] : sendingRange) { const auto msgId = prepareToSend( request, bigMsgId, @@ -904,7 +927,7 @@ void SessionPrivate::tryToSend() { memcpy(toSendRequest->data() + from, request->constData() + 4, len * sizeof(mtpPrime)); } } - toSend.clear(); + toSend.erase(sendingFrom, sendingTill); if (stateRequest) { const auto msgId = placeToContainer( @@ -951,6 +974,11 @@ void SessionPrivate::tryToSend() { } } sendSecureRequest(std::move(toSendRequest), needAnyResponse); + if (someSkipped) { + InvokeQueued(this, [=] { + tryToSend(); + }); + } } void SessionPrivate::retryByTimer() { @@ -1108,9 +1136,6 @@ void SessionPrivate::onSentSome(uint64 size) { DEBUG_LOG(("Checking connect for request with size %1 bytes, delay will be %2").arg(size).arg(remain)); } } - if (isUploadDcId(_shiftedDcId)) { - remain *= kUploadSessionsCount; - } _waitForReceivedTimer.callOnce(remain); } if (!_firstSentAt) { @@ -1577,13 +1602,22 @@ SessionPrivate::HandleResult SessionPrivate::handleOneReceived( correctUnixtimeWithBadLocal(info.serverTime); info.badTime = false; } + if (_bindMsgId) { + LOG(("Message Info: bad message notification received" + " while binding temp key, restarting.")); + return HandleResult::RestartConnection; + } LOG(("Message Info: bad message notification received, msgId %1, error_code %2").arg(data.vbad_msg_id().v).arg(errorCode)); return HandleResult::ResetSession; } } else { // fatal (except 48, but it must not get here) const auto badMsgId = mtpMsgId(data.vbad_msg_id().v); const auto requestId = wasSent(resendId); - if (requestId) { + if (_bindMsgId) { + LOG(("Message Error: fatal bad message notification received" + " while binding temp key, restarting.")); + return HandleResult::RestartConnection; + } else if (requestId) { LOG(("Message Error: " "fatal bad message notification received, " "msgId %1, error_code %2, requestId: %3" @@ -1628,7 +1662,9 @@ SessionPrivate::HandleResult SessionPrivate::handleOneReceived( } _sessionSalt = data.vnew_server_salt().v; - correctUnixtimeWithBadLocal(info.serverTime); + + // Don't force time update here. + base::unixtime::update(info.serverTime); if (_bindMsgId) { LOG(("Message Info: bad_server_salt received while binding temp key, restarting.")); @@ -2056,7 +2092,7 @@ void SessionPrivate::correctUnixtimeByFastRequest( locker.unlock(); SyncTimeRequestDuration = duration; - base::unixtime::update(serverTime, true); + base::unixtime::update(serverTime); return; } } diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp index 684163163..980888ffb 100644 --- a/Telegram/SourceFiles/passport/passport_form_controller.cpp +++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp @@ -1581,14 +1581,10 @@ void FormController::uploadEncryptedFile( &session(), std::make_unique(std::move(data))); - auto prepared = std::make_shared( - TaskId(), - file.uploadData->fileId, - FileLoadTo(PeerId(), Api::SendOptions(), FullReplyTo(), MsgId()), - TextWithTags(), - false, - std::shared_ptr(nullptr)); - prepared->type = SendMediaType::Secure; + auto prepared = MakePreparedFile({ + .id = file.uploadData->fileId, + .type = SendMediaType::Secure, + }); prepared->content = QByteArray::fromRawData( reinterpret_cast(file.uploadData->bytes.data()), file.uploadData->bytes.size()); diff --git a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp index 1242c751f..0ea48a7de 100644 --- a/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/notifications_manager_linux.cpp @@ -37,6 +37,7 @@ namespace Notifications { namespace { using namespace gi::repository; +namespace GObject = gi::repository::GObject; constexpr auto kService = "org.freedesktop.Notifications"; constexpr auto kObjectPath = "/org/freedesktop/Notifications"; diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index 711dda7a9..fe60e831d 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -510,7 +510,7 @@ QString SingleInstanceLocalServerName(const QString &hash) { #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) std::optional IsDarkMode() { - const auto result = base::Platform::XDP::ReadSetting( + auto result = base::Platform::XDP::ReadSetting( "org.freedesktop.appearance", "color-scheme"); diff --git a/Telegram/SourceFiles/settings/business/settings_away_message.cpp b/Telegram/SourceFiles/settings/business/settings_away_message.cpp index afaeb3a2b..58f3a6165 100644 --- a/Telegram/SourceFiles/settings/business/settings_away_message.cpp +++ b/Telegram/SourceFiles/settings/business/settings_away_message.cpp @@ -220,7 +220,7 @@ void AwayMessage::setupContent( _recipients = disabled ? Data::BusinessRecipients{ .allButExcluded = true } - : current.recipients; + : Data::BusinessRecipients::MakeValid(current.recipients); auto initialSchedule = disabled ? AwaySchedule{ .type = AwayScheduleType::Always, } : current.schedule; diff --git a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp index 16072b1f0..7bd61ca90 100644 --- a/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chat_intro.cpp @@ -160,7 +160,7 @@ private: object_ptr( container, st::settingsChatIntroField, - tr::lng_chat_intro_enter_title(), + std::move(placeholder), current), st::settingsChatIntroFieldMargins); field->setMaxLength(limit); diff --git a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp index d31239e19..3c75cf9ba 100644 --- a/Telegram/SourceFiles/settings/business/settings_chatbots.cpp +++ b/Telegram/SourceFiles/settings/business/settings_chatbots.cpp @@ -408,7 +408,7 @@ void Chatbots::setupContent( const auto content = Ui::CreateChild(this); const auto current = controller->session().data().chatbots().current(); - _recipients = current.recipients; + _recipients = Data::BusinessRecipients::MakeValid(current.recipients); _repliesAllowed = current.repliesAllowed; AddDividerTextWithLottie(content, { diff --git a/Telegram/SourceFiles/settings/business/settings_greeting.cpp b/Telegram/SourceFiles/settings/business/settings_greeting.cpp index 052e97a39..8437acc7c 100644 --- a/Telegram/SourceFiles/settings/business/settings_greeting.cpp +++ b/Telegram/SourceFiles/settings/business/settings_greeting.cpp @@ -125,7 +125,7 @@ void Greeting::setupContent( _recipients = disabled ? Data::BusinessRecipients{ .allButExcluded = true } - : current.recipients; + : Data::BusinessRecipients::MakeValid(current.recipients); _noActivityDays = disabled ? kDefaultNoActivityDays : current.noActivityDays; diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 9422c892c..efeffaac9 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -1428,7 +1428,7 @@ void ShortcutMessages::chooseAttach( _choosingAttach = false; const auto filter = (overrideSendImagesAsPhotos == true) - ? FileDialog::ImagesOrAllFilter() + ? FileDialog::PhotoVideoFilesFilter() : FileDialog::AllOrImagesFilter(); FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( FileDialog::OpenResult &&result) { diff --git a/Telegram/SourceFiles/settings/settings_intro.cpp b/Telegram/SourceFiles/settings/settings_intro.cpp index bd38b8c3f..6ddb33f75 100644 --- a/Telegram/SourceFiles/settings/settings_intro.cpp +++ b/Telegram/SourceFiles/settings/settings_intro.cpp @@ -11,10 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_main.h" #include "settings/settings_chat.h" #include "settings/settings_codes.h" +#include "ui/basic_click_handlers.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/widgets/shadow.h" -#include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" #include "ui/widgets/scroll_area.h" #include "ui/cached_round_corners.h" @@ -104,7 +104,14 @@ object_ptr CreateIntroSettings( Ui::AddDivider(result); Ui::AddSkip(result); - SetupFaq(result, false); + + AddButtonWithIcon( + result, + tr::lng_settings_faq(), + st::settingsButtonNoIcon + )->addClickHandler([] { + OpenFaq(nullptr); + }); return result; } diff --git a/Telegram/SourceFiles/settings/settings_main.cpp b/Telegram/SourceFiles/settings/settings_main.cpp index 9a705637f..4ac873853 100644 --- a/Telegram/SourceFiles/settings/settings_main.cpp +++ b/Telegram/SourceFiles/settings/settings_main.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_main.h" #include "core/application.h" +#include "core/click_handler_types.h" #include "settings/settings_business.h" #include "settings/settings_codes.h" #include "settings/settings_chat.h" @@ -26,13 +27,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/basic_click_handlers.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/userpic_button.h" -#include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" -#include "ui/wrap/padding_wrap.h" #include "ui/widgets/menu/menu_add_action_callback.h" -#include "ui/widgets/labels.h" #include "ui/widgets/continuous_sliders.h" -#include "ui/widgets/buttons.h" #include "ui/text/text_utilities.h" #include "ui/toast/toast.h" #include "ui/new_badges.h" @@ -43,9 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "data/data_cloud_themes.h" #include "data/data_chat_filters.h" -#include "data/data_peer_values.h" // Data::AmPremiumValue #include "lang/lang_cloud_manager.h" -#include "lang/lang_keys.h" #include "lang/lang_instance.h" #include "storage/localstorage.h" #include "main/main_session.h" @@ -62,13 +57,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_values.h" #include "window/window_controller.h" #include "window/window_session_controller.h" -#include "core/file_utilities.h" -#include "core/application.h" #include "base/call_delayed.h" -#include "base/unixtime.h" #include "base/platform/base_platform_info.h" #include "styles/style_settings.h" -#include "styles/style_boxes.h" #include "styles/style_info.h" #include "styles/style_menu_icons.h" @@ -229,7 +220,7 @@ void Cover::initViewers() { }, lifetime()); _username->overrideLinkClickHandler([=] { - const auto username = _user->userName(); + const auto username = _user->username(); if (username.isEmpty()) { _controller->show(Box(UsernamesBox, _user)); } else { @@ -625,26 +616,20 @@ void SetupInterfaceScale( } } -void OpenFaq() { - File::OpenUrl(telegramFaqLink()); -} - -void SetupFaq(not_null container, bool icon) { - AddButtonWithIcon( - container, - tr::lng_settings_faq(), - icon ? st::settingsButton : st::settingsButtonNoIcon, - { icon ? &st::menuIconFaq : nullptr } - )->addClickHandler(OpenFaq); -} - void SetupHelp( not_null controller, not_null container) { Ui::AddDivider(container); Ui::AddSkip(container); - SetupFaq(container); + AddButtonWithIcon( + container, + tr::lng_settings_faq(), + st::settingsButton, + { &st::menuIconFaq } + )->addClickHandler([=] { + OpenFaq(controller); + }); AddButtonWithIcon( container, @@ -688,7 +673,10 @@ void SetupHelp( auto box = Ui::MakeConfirmBox({ .text = tr::lng_settings_ask_sure(), .confirmed = sure, - .cancelled = OpenFaq, + .cancelled = [=](Fn close) { + OpenFaq(controller); + close(); + }, .confirmText = tr::lng_settings_ask_ok(), .cancelText = tr::lng_settings_faq_button(), .strictCancel = true, @@ -767,4 +755,12 @@ void Main::setupContent(not_null controller) { controller->session().data().cloudThemes().refresh(); } +void OpenFaq(base::weak_ptr weak) { + UrlClickHandler::Open( + tr::lng_settings_faq_link(tr::now), + QVariant::fromValue(ClickHandlerContext{ + .sessionWindow = weak, + })); +} + } // namespace Settings diff --git a/Telegram/SourceFiles/settings/settings_main.h b/Telegram/SourceFiles/settings/settings_main.h index 7040fb9fa..a0f7e15ca 100644 --- a/Telegram/SourceFiles/settings/settings_main.h +++ b/Telegram/SourceFiles/settings/settings_main.h @@ -28,9 +28,8 @@ void SetupInterfaceScale( not_null window, not_null container, bool icon = true); -void SetupFaq( - not_null container, - bool icon = true); + +void OpenFaq(base::weak_ptr weak); class Main : public Section
{ public: diff --git a/Telegram/SourceFiles/storage/file_upload.cpp b/Telegram/SourceFiles/storage/file_upload.cpp index 97a1ce913..95c4b0a1b 100644 --- a/Telegram/SourceFiles/storage/file_upload.cpp +++ b/Telegram/SourceFiles/storage/file_upload.cpp @@ -23,15 +23,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_session.h" #include "apiwrap.h" -// AyuGram includes -#include "ayu/ayu_settings.h" - - namespace Storage { namespace { -// max 512kb uploaded at the same time in each session -constexpr auto kMaxUploadFileParallelSize = MTP::kUploadSessionsCount * 512 * 1024; +// max 1mb uploaded at the same time in each session +constexpr auto kMaxUploadPerSession = 1024 * 1024; constexpr auto kDocumentMaxPartsCountDefault = 4000; @@ -51,84 +47,89 @@ constexpr auto kDocumentUploadPartSize3 = 256 * 1024; constexpr auto kDocumentUploadPartSize4 = 512 * 1024; // One part each half second, if not uploaded faster. -constexpr auto kUploadRequestInterval = crl::time(500); +constexpr auto kUploadRequestInterval = crl::time(250); // How much time without upload causes additional session kill. constexpr auto kKillSessionTimeout = 15 * crl::time(1000); +// How much wait after session kill before killing another one. +constexpr auto kWaitForNormalizeTimeout = 8 * crl::time(1000); + +constexpr auto kMaxSessionsCount = 8; +constexpr auto kFastRequestThreshold = 1 * crl::time(1000); +constexpr auto kSlowRequestThreshold = 8 * crl::time(1000); + +// Request is 'fast' if it was done in less than 1s and +// (it-s size + queued before size) >= 512kb. +constexpr auto kAcceptAsFastIfTotalAtLeast = 512 * 1024; + [[nodiscard]] const char *ThumbnailFormat(const QString &mime) { return Core::IsMimeSticker(mime) ? "WEBP" : "JPG"; } -int UploadSessionsCount() { - const auto settings = &AyuSettings::getInstance(); - static const auto count = settings->uploadSpeedBoost ? 8 : 2; - return count; -} - -int UploadSessionsInterval() { - const auto settings = &AyuSettings::getInstance(); - static const auto interval = settings->uploadSpeedBoost ? 25 : kUploadRequestInterval; - return interval; -} - } // namespace -struct Uploader::File { - File(const SendMediaReady &media); - File(const std::shared_ptr &file); +struct Uploader::Entry { + Entry(FullMsgId itemId, const std::shared_ptr &file); void setDocSize(int64 size); - bool setPartSize(uint32 partSize); + bool setPartSize(int partSize); - std::shared_ptr file; - SendMediaReady media; - int32 partsCount = 0; - mutable int64 fileSentSize = 0; + // const, but non-const for the move-assignment in the + FullMsgId itemId; + std::shared_ptr file; + not_null*> parts; + uint64 partsOfId = 0; - uint64 id() const; - SendMediaType type() const; - uint64 thumbId() const; - const QString &filename() const; + int64 sentSize = 0; + ushort partsSent = 0; + ushort partsWaiting = 0; HashMd5 md5Hash; std::unique_ptr docFile; int64 docSize = 0; - int64 docPartSize = 0; - int docSentParts = 0; - int docPartsCount = 0; + int64 docSentSize = 0; + int docPartSize = 0; + ushort docPartsSent = 0; + ushort docPartsCount = 0; + ushort docPartsWaiting = 0; }; -Uploader::File::File(const SendMediaReady &media) : media(media) { - partsCount = media.parts.size(); - if (type() == SendMediaType::File - || type() == SendMediaType::ThemeFile - || type() == SendMediaType::Audio) { - setDocSize(media.file.isEmpty() - ? media.data.size() - : media.filesize); - } else { - docSize = docPartSize = docPartsCount = 0; - } -} -Uploader::File::File(const std::shared_ptr &file) -: file(file) { - partsCount = (type() == SendMediaType::Photo - || type() == SendMediaType::Secure) - ? file->fileparts.size() - : file->thumbparts.size(); - if (type() == SendMediaType::File - || type() == SendMediaType::ThemeFile - || type() == SendMediaType::Audio) { +struct Uploader::Request { + FullMsgId itemId; + crl::time sent = 0; + QByteArray bytes; + int queued = 0; + ushort part = 0; + uchar dcIndex = 0; + bool docPart = false; + bool bigPart = false; + bool nonPremiumDelayed = false; +}; + +Uploader::Entry::Entry( + FullMsgId itemId, + const std::shared_ptr &file) +: itemId(itemId) +, file(file) +, parts((file->type == SendMediaType::Photo + || file->type == SendMediaType::Secure) + ? &file->fileparts + : &file->thumbparts) +, partsOfId((file->type == SendMediaType::Photo + || file->type == SendMediaType::Secure) + ? file->id + : file->thumbId) { + if (file->type == SendMediaType::File + || file->type == SendMediaType::ThemeFile + || file->type == SendMediaType::Audio) { setDocSize(file->filesize); - } else { - docSize = docPartSize = docPartsCount = 0; } } -void Uploader::File::setDocSize(int64 size) { +void Uploader::Entry::setDocSize(int64 size) { docSize = size; constexpr auto limit0 = 1024 * 1024; constexpr auto limit1 = 32 * limit0; @@ -143,32 +144,15 @@ void Uploader::File::setDocSize(int64 size) { } } -bool Uploader::File::setPartSize(uint32 partSize) { +bool Uploader::Entry::setPartSize(int partSize) { docPartSize = partSize; - docPartsCount = (docSize / docPartSize) - + ((docSize % docPartSize) ? 1 : 0); + docPartsCount = (docSize + docPartSize - 1) / docPartSize; return (docPartsCount <= kDocumentMaxPartsCountDefault); } -uint64 Uploader::File::id() const { - return file ? file->id : media.id; -} - -SendMediaType Uploader::File::type() const { - return file ? file->type : media.type; -} - -uint64 Uploader::File::thumbId() const { - return file ? file->thumbId : media.thumbId; -} - -const QString &Uploader::File::filename() const { - return file ? file->filename : media.filename; -} - Uploader::Uploader(not_null api) : _api(api) -, _nextTimer([=] { sendNext(); }) +, _nextTimer([=] { maybeSend(); }) , _stopSessionsTimer([=] { stopSessions(); }) { const auto session = &_api->session(); photoReady( @@ -225,22 +209,21 @@ Uploader::Uploader(not_null api) _api->instance().nonPremiumDelayedRequests( ) | rpl::start_with_next([=](mtpRequestId id) { - if (dcMap.contains(id)) { - _nonPremiumDelayed.emplace(id); + const auto i = _requests.find(id); + if (i != end(_requests)) { + i->second.nonPremiumDelayed = true; } }, _lifetime); } -void Uploader::processPhotoProgress(const FullMsgId &newId) { - const auto session = &_api->session(); - if (const auto item = session->data().message(newId)) { +void Uploader::processPhotoProgress(FullMsgId itemId) { + if (const auto item = session().data().message(itemId)) { sendProgressUpdate(item, Api::SendProgressType::UploadPhoto); } } -void Uploader::processDocumentProgress(const FullMsgId &newId) { - const auto session = &_api->session(); - if (const auto item = session->data().message(newId)) { +void Uploader::processDocumentProgress(FullMsgId itemId) { + if (const auto item = session().data().message(itemId)) { const auto media = item->media(); const auto document = media ? media->document() : nullptr; const auto sendAction = (document && document->isVoiceMessage()) @@ -254,16 +237,14 @@ void Uploader::processDocumentProgress(const FullMsgId &newId) { } } -void Uploader::processPhotoFailed(const FullMsgId &newId) { - const auto session = &_api->session(); - if (const auto item = session->data().message(newId)) { +void Uploader::processPhotoFailed(FullMsgId itemId) { + if (const auto item = session().data().message(itemId)) { sendProgressUpdate(item, Api::SendProgressType::UploadPhoto, -1); } } -void Uploader::processDocumentFailed(const FullMsgId &newId) { - const auto session = &_api->session(); - if (const auto item = session->data().message(newId)) { +void Uploader::processDocumentFailed(FullMsgId itemId) { + if (const auto item = session().data().message(itemId)) { const auto media = item->media(); const auto document = media ? media->document() : nullptr; const auto sendAction = (document && document->isVoiceMessage()) @@ -298,39 +279,13 @@ Main::Session &Uploader::session() const { return _api->session(); } -void Uploader::uploadMedia( - const FullMsgId &msgId, - const SendMediaReady &media) { - if (media.type == SendMediaType::Photo) { - session().data().processPhoto(media.photo, media.photoThumbs); - } else if (media.type == SendMediaType::File - || media.type == SendMediaType::ThemeFile - || media.type == SendMediaType::Audio) { - const auto document = media.photoThumbs.empty() - ? session().data().processDocument(media.document) - : session().data().processDocument( - media.document, - Images::FromImageInMemory( - media.photoThumbs.front().second.image, - "JPG", - media.photoThumbs.front().second.bytes)); - if (!media.data.isEmpty()) { - document->setDataAndCache(media.data); - if (media.type == SendMediaType::ThemeFile) { - document->checkWallPaperProperties(); - } - } - if (!media.file.isEmpty()) { - document->setLocation(Core::FileLocation(media.file)); - } - } - queue.emplace(msgId, File(media)); - sendNext(); +FullMsgId Uploader::currentUploadId() const { + return _queue.empty() ? FullMsgId() : _queue.front().itemId; } void Uploader::upload( - const FullMsgId &msgId, - const std::shared_ptr &file) { + FullMsgId itemId, + const std::shared_ptr &file) { if (file->type == SendMediaType::Photo) { const auto photo = session().data().processPhoto( file->photo, @@ -375,414 +330,580 @@ void Uploader::upload( document->checkWallPaperProperties(); } } - queue.emplace(msgId, File(file)); - sendNext(); + _queue.push_back({ itemId, file }); + if (!_nextTimer.isActive()) { + maybeSend(); + } } -void Uploader::currentFailed() { - auto j = queue.find(uploadingId); - if (j != queue.end()) { - const auto [msgId, file] = std::move(*j); - queue.erase(j); - notifyFailed(msgId, file); +void Uploader::failed(FullMsgId itemId) { + const auto i = ranges::find(_queue, itemId, &Entry::itemId); + if (i != end(_queue)) { + const auto entry = std::move(*i); + _queue.erase(i); + notifyFailed(entry); } - - cancelRequests(); - dcMap.clear(); - uploadingId = FullMsgId(); - sentSize = 0; - for (int i = 0; i < UploadSessionsCount(); ++i) { - sentSizes[i] = 0; - } - - sendNext(); + cancelRequests(itemId); + maybeFinishFront(); + crl::on_main(this, [=] { + maybeSend(); + }); } -void Uploader::notifyFailed(FullMsgId id, const File &file) { - const auto type = file.type(); +void Uploader::notifyFailed(const Entry &entry) { + const auto type = entry.file->type; if (type == SendMediaType::Photo) { - _photoFailed.fire_copy(id); + _photoFailed.fire_copy(entry.itemId); } else if (type == SendMediaType::File || type == SendMediaType::ThemeFile || type == SendMediaType::Audio) { - const auto document = session().data().document(file.id()); + const auto document = session().data().document(entry.file->id); if (document->uploading()) { document->status = FileUploadFailed; } - _documentFailed.fire_copy(id); + _documentFailed.fire_copy(entry.itemId); } else if (type == SendMediaType::Secure) { - _secureFailed.fire_copy(id); + _secureFailed.fire_copy(entry.itemId); } else { - Unexpected("Type in Uploader::currentFailed."); + Unexpected("Type in Uploader::failed."); } } void Uploader::stopSessions() { - for (int i = 0; i < UploadSessionsCount(); ++i) { - _api->instance().stopSession(MTP::uploadDcId(i)); + if (ranges::any_of(_sentPerDcIndex, rpl::mappers::_1 != 0)) { + _stopSessionsTimer.callOnce(kKillSessionTimeout); + } else { + for (auto i = 0; i != int(_sentPerDcIndex.size()); ++i) { + _api->instance().stopSession(MTP::uploadDcId(i)); + } + _sentPerDcIndex.clear(); + _dcIndicesWithFastRequests.clear(); } } -void Uploader::sendNext() { - if (sentSize >= UploadSessionsCount() * 512 * 1024 || _pausedId.msg) { - return; +QByteArray Uploader::readDocPart(not_null entry) { + const auto checked = [&](QByteArray result) { + if ((entry->file->type == SendMediaType::File + || entry->file->type == SendMediaType::ThemeFile + || entry->file->type == SendMediaType::Audio) + && entry->docSize <= kUseBigFilesFrom) { + entry->md5Hash.feed(result.data(), result.size()); + } + if (result.isEmpty() + || (result.size() > entry->docPartSize) + || ((result.size() < entry->docPartSize + && entry->docPartsSent + 1 != entry->docPartsCount))) { + return QByteArray(); + } + return result; + }; + auto &content = entry->file->content; + if (!content.isEmpty()) { + const auto offset = entry->docPartsSent * entry->docPartSize; + return checked(content.mid(offset, entry->docPartSize)); + } else if (!entry->docFile) { + const auto filepath = entry->file->filepath; + entry->docFile = std::make_unique(filepath); + if (!entry->docFile->open(QIODevice::ReadOnly)) { + return QByteArray(); + } + } + return checked(entry->docFile->read(entry->docPartSize)); +} + +bool Uploader::canAddDcIndex() const { + const auto count = int(_sentPerDcIndex.size()); + return (count < kMaxSessionsCount) + && (count == int(_dcIndicesWithFastRequests.size())); +} + +std::optional Uploader::chooseDcIndexForNextRequest( + const base::flat_set &used) { + for (auto i = 0, count = int(_sentPerDcIndex.size()); i != count; ++i) { + if (!_sentPerDcIndex[i] && !used.contains(i)) { + return i; + } + } + if (canAddDcIndex()) { + const auto result = int(_sentPerDcIndex.size()); + _sentPerDcIndex.push_back(0); + _dcIndicesWithFastRequests.clear(); + _latestDcIndexAdded = crl::now(); + + DEBUG_LOG(("Uploader: Added dc index %1.").arg(result)); + return result; + } + auto result = std::optional(); + for (auto i = 0, count = int(_sentPerDcIndex.size()); i != count; ++i) { + if (!used.contains(i) + && (!result.has_value() + || _sentPerDcIndex[i] < _sentPerDcIndex[*result])) { + result = i; + } + } + return result; +} + +Uploader::Entry *Uploader::chooseEntryForNextRequest() { + if (!_pendingFromRemovedDcIndices.empty()) { + const auto itemId = _pendingFromRemovedDcIndices.front().itemId; + const auto i = ranges::find(_queue, itemId, &Entry::itemId); + Assert(i != end(_queue)); + return &*i; } + for (auto i = begin(_queue); i != end(_queue); ++i) { + if (i->partsSent < i->parts->size() + || i->docPartsSent < i->docPartsCount) { + return &*i; + } + } + return nullptr; +} + +auto Uploader::sendPart(not_null entry, uchar dcIndex) +-> SendResult { + return !_pendingFromRemovedDcIndices.empty() + ? sendPendingPart(entry, dcIndex) + : (entry->partsSent < entry->parts->size()) + ? sendSlicedPart(entry, dcIndex) + : sendDocPart(entry, dcIndex); +} + +template +void Uploader::sendPreparedRequest(Prepared &&prepared, Request &&request) { + auto &sentInSession = _sentPerDcIndex[request.dcIndex]; + const auto queued = sentInSession; + sentInSession += int(request.bytes.size()); + + const auto requestId = _api->request( + std::move(prepared) + ).done([=](const MTPBool &result, mtpRequestId requestId) { + partLoaded(result, requestId); + }).fail([=](const MTP::Error &error, mtpRequestId requestId) { + partFailed(error, requestId); + }).toDC(MTP::uploadDcId(request.dcIndex)).send(); + + request.sent = crl::now(); + request.queued = queued; + _requests.emplace(requestId, std::move(request)); +} + +auto Uploader::sendPendingPart(not_null entry, uchar dcIndex) +-> SendResult { + Expects(!_pendingFromRemovedDcIndices.empty()); + Expects(_pendingFromRemovedDcIndices.front().itemId == entry->itemId); + + auto request = std::move(_pendingFromRemovedDcIndices.front()); + _pendingFromRemovedDcIndices.erase(begin(_pendingFromRemovedDcIndices)); + + const auto part = request.part; + const auto bytes = request.bytes; + request.dcIndex = dcIndex; + if (request.bigPart) { + sendPreparedRequest(MTPupload_SaveBigFilePart( + MTP_long(entry->file->id), + MTP_int(part), + MTP_int(entry->docPartsCount), + MTP_bytes(bytes) + ), std::move(request)); + } else { + const auto id = request.docPart ? entry->file->id : entry->partsOfId; + sendPreparedRequest(MTPupload_SaveFilePart( + MTP_long(id), + MTP_int(part), + MTP_bytes(bytes) + ), std::move(request)); + } + return SendResult::Success; +} + +auto Uploader::sendDocPart(not_null entry, uchar dcIndex) +-> SendResult { + const auto itemId = entry->itemId; + const auto alreadySent = _sentPerDcIndex[dcIndex]; + const auto willProbablyBeSent = entry->docPartSize; + if (alreadySent + willProbablyBeSent > kMaxUploadPerSession) { + return SendResult::DcIndexFull; + } + + Assert(entry->docPartsSent < entry->docPartsCount); + + const auto partBytes = readDocPart(entry); + if (partBytes.isEmpty()) { + failed(itemId); + return SendResult::Failed; + } + const auto part = entry->docPartsSent++; + ++entry->docPartsWaiting; + + const auto send = [&](auto &&request, bool big) { + sendPreparedRequest(std::move(request), { + .itemId = itemId, + .bytes = partBytes, + .part = part, + .dcIndex = dcIndex, + .docPart = true, + .bigPart = big, + }); + }; + if (entry->docSize > kUseBigFilesFrom) { + send(MTPupload_SaveBigFilePart( + MTP_long(entry->file->id), + MTP_int(part), + MTP_int(entry->docPartsCount), + MTP_bytes(partBytes) + ), true); + } else { + send(MTPupload_SaveFilePart( + MTP_long(entry->file->id), + MTP_int(part), + MTP_bytes(partBytes) + ), false); + } + return SendResult::Success; +} + +auto Uploader::sendSlicedPart(not_null entry, uchar dcIndex) +-> SendResult { + const auto itemId = entry->itemId; + const auto alreadySent = _sentPerDcIndex[dcIndex]; + const auto willBeSent = entry->parts->at(entry->partsSent).size(); + if (alreadySent + willBeSent >= kMaxUploadPerSession) { + return SendResult::DcIndexFull; + } + + ++entry->partsWaiting; + const auto index = entry->partsSent++; + const auto partBytes = entry->parts->at(index); + sendPreparedRequest(MTPupload_SaveFilePart( + MTP_long(entry->partsOfId), + MTP_int(index), + MTP_bytes(partBytes) + ), { + .itemId = itemId, + .bytes = partBytes, + .dcIndex = dcIndex, + }); + return SendResult::Success; +} + +void Uploader::maybeSend() { const auto stopping = _stopSessionsTimer.isActive(); - if (queue.empty()) { + if (_queue.empty()) { if (!stopping) { _stopSessionsTimer.callOnce(kKillSessionTimeout); } + _pausedId = FullMsgId(); return; - } - - if (stopping) { + } else if (_pausedId) { + return; + } else if (stopping) { _stopSessionsTimer.cancel(); } - auto i = uploadingId.msg ? queue.find(uploadingId) : queue.begin(); - if (!uploadingId.msg) { - uploadingId = i->first; - } else if (i == queue.end()) { - i = queue.begin(); - uploadingId = i->first; - } - auto &uploadingData = i->second; - auto todc = 0; - for (auto dc = 1; dc != UploadSessionsCount(); ++dc) { - if (sentSizes[dc] < sentSizes[todc]) { - todc = dc; + auto usedDcIndices = base::flat_set(); + while (true) { + const auto maybeDcIndex = chooseDcIndexForNextRequest(usedDcIndices); + if (!maybeDcIndex.has_value()) { + break; + } + const auto dcIndex = *maybeDcIndex; + while (true) { + const auto entry = chooseEntryForNextRequest(); + if (!entry) { + return; + } + const auto result = sendPart(entry, dcIndex); + if (result == SendResult::DcIndexFull) { + return; + } else if (result == SendResult::Success) { + break; + } + // If this entry failed, we try the next one. + } + if (_sentPerDcIndex[dcIndex] >= kAcceptAsFastIfTotalAtLeast) { + usedDcIndices.emplace(dcIndex); } } - - auto &parts = uploadingData.file - ? ((uploadingData.type() == SendMediaType::Photo - || uploadingData.type() == SendMediaType::Secure) - ? uploadingData.file->fileparts - : uploadingData.file->thumbparts) - : uploadingData.media.parts; - const auto partsOfId = uploadingData.file - ? ((uploadingData.type() == SendMediaType::Photo - || uploadingData.type() == SendMediaType::Secure) - ? uploadingData.file->id - : uploadingData.file->thumbId) - : uploadingData.media.thumbId; - if (parts.isEmpty()) { - if (uploadingData.docSentParts >= uploadingData.docPartsCount) { - if (requestsSent.empty() && docRequestsSent.empty()) { - const auto options = uploadingData.file - ? uploadingData.file->to.options - : Api::SendOptions(); - const auto edit = uploadingData.file && - uploadingData.file->to.replaceMediaOf; - const auto attachedStickers = uploadingData.file - ? uploadingData.file->attachedStickers - : std::vector(); - if (uploadingData.type() == SendMediaType::Photo) { - auto photoFilename = uploadingData.filename(); - if (!photoFilename.endsWith(u".jpg"_q, Qt::CaseInsensitive)) { - // Server has some extensions checking for inputMediaUploadedPhoto, - // so force the extension to be .jpg anyway. It doesn't matter, - // because the filename from inputFile is not used anywhere. - photoFilename += u".jpg"_q; - } - const auto md5 = uploadingData.file - ? uploadingData.file->filemd5 - : uploadingData.media.jpeg_md5; - const auto file = MTP_inputFile( - MTP_long(uploadingData.id()), - MTP_int(uploadingData.partsCount), - MTP_string(photoFilename), - MTP_bytes(md5)); - _photoReady.fire({ - .fullId = uploadingId, - .info = { - .file = file, - .attachedStickers = attachedStickers, - }, - .options = options, - .edit = edit, - }); - } else if (uploadingData.type() == SendMediaType::File - || uploadingData.type() == SendMediaType::ThemeFile - || uploadingData.type() == SendMediaType::Audio) { - QByteArray docMd5(32, Qt::Uninitialized); - hashMd5Hex(uploadingData.md5Hash.result(), docMd5.data()); - - const auto file = (uploadingData.docSize > kUseBigFilesFrom) - ? MTP_inputFileBig( - MTP_long(uploadingData.id()), - MTP_int(uploadingData.docPartsCount), - MTP_string(uploadingData.filename())) - : MTP_inputFile( - MTP_long(uploadingData.id()), - MTP_int(uploadingData.docPartsCount), - MTP_string(uploadingData.filename()), - MTP_bytes(docMd5)); - const auto thumb = [&]() -> std::optional { - if (!uploadingData.partsCount) { - return std::nullopt; - } - const auto thumbFilename = uploadingData.file - ? uploadingData.file->thumbname - : (u"thumb."_q + uploadingData.media.thumbExt); - const auto thumbMd5 = uploadingData.file - ? uploadingData.file->thumbmd5 - : uploadingData.media.jpeg_md5; - return MTP_inputFile( - MTP_long(uploadingData.thumbId()), - MTP_int(uploadingData.partsCount), - MTP_string(thumbFilename), - MTP_bytes(thumbMd5)); - }(); - _documentReady.fire({ - .fullId = uploadingId, - .info = { - .file = file, - .thumb = thumb, - .attachedStickers = attachedStickers, - }, - .options = options, - .edit = edit, - }); - } else if (uploadingData.type() == SendMediaType::Secure) { - _secureReady.fire({ - uploadingId, - uploadingData.id(), - uploadingData.partsCount }); - } - queue.erase(uploadingId); - uploadingId = FullMsgId(); - sendNext(); - } - return; - } - - auto &content = uploadingData.file - ? uploadingData.file->content - : uploadingData.media.data; - QByteArray toSend; - if (content.isEmpty()) { - if (!uploadingData.docFile) { - const auto filepath = uploadingData.file - ? uploadingData.file->filepath - : uploadingData.media.file; - uploadingData.docFile = std::make_unique(filepath); - if (!uploadingData.docFile->open(QIODevice::ReadOnly)) { - currentFailed(); - return; - } - } - toSend = uploadingData.docFile->read(uploadingData.docPartSize); - if (uploadingData.docSize <= kUseBigFilesFrom) { - uploadingData.md5Hash.feed(toSend.constData(), toSend.size()); - } - } else { - const auto offset = uploadingData.docSentParts - * uploadingData.docPartSize; - toSend = content.mid(offset, uploadingData.docPartSize); - if ((uploadingData.type() == SendMediaType::File - || uploadingData.type() == SendMediaType::ThemeFile - || uploadingData.type() == SendMediaType::Audio) - && uploadingData.docSentParts <= kUseBigFilesFrom) { - uploadingData.md5Hash.feed(toSend.constData(), toSend.size()); - } - } - if ((toSend.size() > uploadingData.docPartSize) - || ((toSend.size() < uploadingData.docPartSize - && uploadingData.docSentParts + 1 != uploadingData.docPartsCount))) { - currentFailed(); - return; - } - mtpRequestId requestId; - if (uploadingData.docSize > kUseBigFilesFrom) { - requestId = _api->request(MTPupload_SaveBigFilePart( - MTP_long(uploadingData.id()), - MTP_int(uploadingData.docSentParts), - MTP_int(uploadingData.docPartsCount), - MTP_bytes(toSend) - )).done([=](const MTPBool &result, mtpRequestId requestId) { - partLoaded(result, requestId); - }).fail([=](const MTP::Error &error, mtpRequestId requestId) { - partFailed(error, requestId); - }).toDC(MTP::uploadDcId(todc)).send(); - } else { - requestId = _api->request(MTPupload_SaveFilePart( - MTP_long(uploadingData.id()), - MTP_int(uploadingData.docSentParts), - MTP_bytes(toSend) - )).done([=](const MTPBool &result, mtpRequestId requestId) { - partLoaded(result, requestId); - }).fail([=](const MTP::Error &error, mtpRequestId requestId) { - partFailed(error, requestId); - }).toDC(MTP::uploadDcId(todc)).send(); - } - docRequestsSent.emplace(requestId, uploadingData.docSentParts); - dcMap.emplace(requestId, todc); - sentSize += uploadingData.docPartSize; - sentSizes[todc] += uploadingData.docPartSize; - - uploadingData.docSentParts++; + if (usedDcIndices.empty()) { + _nextTimer.cancel(); } else { - auto part = parts.begin(); - - const auto requestId = _api->request(MTPupload_SaveFilePart( - MTP_long(partsOfId), - MTP_int(part.key()), - MTP_bytes(part.value()) - )).done([=](const MTPBool &result, mtpRequestId requestId) { - partLoaded(result, requestId); - }).fail([=](const MTP::Error &error, mtpRequestId requestId) { - partFailed(error, requestId); - }).toDC(MTP::uploadDcId(todc)).send(); - requestsSent.emplace(requestId, part.value()); - dcMap.emplace(requestId, todc); - sentSize += part.value().size(); - sentSizes[todc] += part.value().size(); - - parts.erase(part); + _nextTimer.callOnce(kUploadRequestInterval); } - _nextTimer.callOnce(crl::time(UploadSessionsInterval())); } -void Uploader::cancel(const FullMsgId &msgId) { - if (uploadingId == msgId) { - currentFailed(); - } else { - queue.erase(msgId); - } +void Uploader::cancel(FullMsgId itemId) { + failed(itemId); } void Uploader::cancelAll() { - const auto single = queue.empty() ? uploadingId : queue.begin()->first; - if (!single) { - return; - } - _pausedId = single; - if (uploadingId) { - currentFailed(); - } - while (!queue.empty()) { - const auto [msgId, file] = std::move(*queue.begin()); - queue.erase(queue.begin()); - notifyFailed(msgId, file); + while (!_queue.empty()) { + failed(_queue.front().itemId); } clear(); unpause(); } -void Uploader::pause(const FullMsgId &msgId) { - _pausedId = msgId; +void Uploader::pause(FullMsgId itemId) { + _pausedId = itemId; } void Uploader::unpause() { _pausedId = FullMsgId(); - sendNext(); + maybeSend(); } -void Uploader::confirm(const FullMsgId &msgId) { +void Uploader::cancelRequests(FullMsgId itemId) { + for (auto i = begin(_requests); i != end(_requests);) { + if (i->second.itemId == itemId) { + const auto bytes = int(i->second.bytes.size()); + _sentPerDcIndex[i->second.dcIndex] -= bytes; + _api->request(i->first).cancel(); + i = _requests.erase(i); + } else { + ++i; + } + } + _pendingFromRemovedDcIndices.erase(ranges::remove( + _pendingFromRemovedDcIndices, + itemId, + &Request::itemId + ), end(_pendingFromRemovedDcIndices)); } -void Uploader::cancelRequests() { - for (const auto &requestData : requestsSent) { - _api->request(requestData.first).cancel(); +void Uploader::cancelAllRequests() { + for (const auto &[requestId, request] : base::take(_requests)) { + _api->request(requestId).cancel(); } - requestsSent.clear(); - for (const auto &requestData : docRequestsSent) { - _api->request(requestData.first).cancel(); - } - docRequestsSent.clear(); + ranges::fill(_sentPerDcIndex, 0); } void Uploader::clear() { - queue.clear(); - cancelRequests(); - dcMap.clear(); - sentSize = 0; - for (int i = 0; i < UploadSessionsCount(); ++i) { - _api->instance().stopSession(MTP::uploadDcId(i)); - sentSizes[i] = 0; - } + _queue.clear(); + cancelAllRequests(); + stopSessions(); _stopSessionsTimer.cancel(); } -void Uploader::partLoaded(const MTPBool &result, mtpRequestId requestId) { - auto j = docRequestsSent.end(); - auto i = requestsSent.find(requestId); - if (i == requestsSent.cend()) { - j = docRequestsSent.find(requestId); - } - const auto wasNonPremiumDelayed = _nonPremiumDelayed.remove(requestId); - if (i != requestsSent.cend() || j != docRequestsSent.cend()) { - if (mtpIsFalse(result)) { // failed to upload current file - currentFailed(); - return; - } else { - auto dcIt = dcMap.find(requestId); - if (dcIt == dcMap.cend()) { // must not happen - currentFailed(); - return; - } - auto dc = dcIt->second; - dcMap.erase(dcIt); +Uploader::Request Uploader::finishRequest(mtpRequestId requestId) { + const auto taken = _requests.take(requestId); + Assert(taken.has_value()); - int64 sentPartSize = 0; - auto k = queue.find(uploadingId); - Assert(k != queue.cend()); - auto &[fullId, file] = *k; - if (i != requestsSent.cend()) { - sentPartSize = i->second.size(); - requestsSent.erase(i); + _sentPerDcIndex[taken->dcIndex] -= int(taken->bytes.size()); + return *taken; +} + +void Uploader::partLoaded(const MTPBool &result, mtpRequestId requestId) { + const auto request = finishRequest(requestId); + + const auto bytes = int(request.bytes.size()); + const auto itemId = request.itemId; + + if (mtpIsFalse(result)) { // failed to upload current file + failed(itemId); + return; + } + + const auto i = ranges::find(_queue, itemId, &Entry::itemId); + Assert(i != end(_queue)); + auto &entry = *i; + + const auto now = crl::now(); + const auto duration = now - request.sent; + const auto fast = (duration < kFastRequestThreshold); + const auto slowish = !fast; + const auto slow = (duration >= kSlowRequestThreshold); + + if (slowish) { + _dcIndicesWithFastRequests.clear(); + if (slow) { + const auto elapsed = (now - _latestDcIndexRemoved); + const auto remove = (elapsed >= kWaitForNormalizeTimeout); + if (remove && _sentPerDcIndex.size() > 1) { + DEBUG_LOG(("Uploader: Slow request, removing dc index.")); + removeDcIndex(); + _latestDcIndexRemoved = now; } else { - sentPartSize = file.docPartSize; - docRequestsSent.erase(j); - } - sentSize -= sentPartSize; - sentSizes[dc] -= sentPartSize; - if (file.type() == SendMediaType::Photo) { - file.fileSentSize += sentPartSize; - const auto photo = session().data().photo(file.id()); - if (photo->uploading() && file.file) { - photo->uploadingData->size = file.file->partssize; - photo->uploadingData->offset = file.fileSentSize; - } - _photoProgress.fire_copy(fullId); - } else if (file.type() == SendMediaType::File - || file.type() == SendMediaType::ThemeFile - || file.type() == SendMediaType::Audio) { - const auto document = session().data().document(file.id()); - if (document->uploading()) { - const auto doneParts = file.docSentParts - - int(docRequestsSent.size()); - document->uploadingData->offset = std::min( - document->uploadingData->size, - doneParts * file.docPartSize); - } - _documentProgress.fire_copy(fullId); - } else if (file.type() == SendMediaType::Secure) { - file.fileSentSize += sentPartSize; - _secureProgress.fire_copy({ - fullId, - file.fileSentSize, - file.file->partssize }); - } - if (wasNonPremiumDelayed) { - _nonPremiumDelays.fire_copy(fullId); + DEBUG_LOG(("Uploader: Slow request, clear fast records.")); } + } else { + DEBUG_LOG(("Uploader: Slow-ish request, clear fast records.")); + } + } else if (request.sent > _latestDcIndexAdded + && (request.queued + bytes >= kAcceptAsFastIfTotalAtLeast)) { + if (_dcIndicesWithFastRequests.emplace(request.dcIndex).second) { + DEBUG_LOG(("Uploader: Mark %1 of %2 as fast." + ).arg(request.dcIndex + ).arg(_sentPerDcIndex.size())); } } - sendNext(); + if (request.docPart) { + --entry.docPartsWaiting; + entry.docSentSize += bytes; + } else { + --entry.partsWaiting; + entry.sentSize += bytes; + } + + if (entry.file->type == SendMediaType::Photo) { + const auto photo = session().data().photo(entry.file->id); + if (photo->uploading()) { + photo->uploadingData->size = entry.file->partssize; + photo->uploadingData->offset = entry.sentSize; + } + _photoProgress.fire_copy(itemId); + } else if (entry.file->type == SendMediaType::File + || entry.file->type == SendMediaType::ThemeFile + || entry.file->type == SendMediaType::Audio) { + const auto document = session().data().document(entry.file->id); + if (document->uploading()) { + document->uploadingData->offset = std::min( + document->uploadingData->size, + entry.docSentSize); + } + _documentProgress.fire_copy(itemId); + } else if (entry.file->type == SendMediaType::Secure) { + _secureProgress.fire_copy({ + .fullId = itemId, + .offset = entry.sentSize, + .size = entry.file->partssize, + }); + } + if (request.nonPremiumDelayed) { + _nonPremiumDelays.fire_copy(itemId); + } + + if (!_queue.empty() && itemId == _queue.front().itemId) { + maybeFinishFront(); + } + maybeSend(); +} + +void Uploader::removeDcIndex() { + Expects(_sentPerDcIndex.size() > 1); + + const auto dcIndex = int(_sentPerDcIndex.size()) - 1; + for (auto i = begin(_requests); i != end(_requests);) { + if (i->second.dcIndex == dcIndex) { + const auto bytes = int(i->second.bytes.size()); + _sentPerDcIndex[dcIndex] -= bytes; + _api->request(i->first).cancel(); + _pendingFromRemovedDcIndices.push_back(std::move(i->second)); + i = _requests.erase(i); + } else { + ++i; + } + } + Assert(_sentPerDcIndex.back() == 0); + _sentPerDcIndex.pop_back(); + _dcIndicesWithFastRequests.remove(dcIndex); + _api->instance().stopSession(MTP::uploadDcId(dcIndex)); + DEBUG_LOG(("Uploader: Removed dc index %1.").arg(dcIndex)); +} + +void Uploader::maybeFinishFront() { + while (!_queue.empty()) { + const auto &entry = _queue.front(); + if (entry.partsSent >= entry.parts->size() + && entry.docPartsSent >= entry.docPartsCount + && !entry.partsWaiting + && !entry.docPartsWaiting) { + finishFront(); + } else { + break; + } + } +} + +void Uploader::finishFront() { + Expects(!_queue.empty()); + + auto entry = std::move(_queue.front()); + _queue.erase(_queue.begin()); + + const auto options = entry.file + ? entry.file->to.options + : Api::SendOptions(); + const auto edit = entry.file && + entry.file->to.replaceMediaOf; + const auto attachedStickers = entry.file + ? entry.file->attachedStickers + : std::vector(); + if (entry.file->type == SendMediaType::Photo) { + auto photoFilename = entry.file->filename; + if (!photoFilename.endsWith(u".jpg"_q, Qt::CaseInsensitive)) { + // Server has some extensions checking for inputMediaUploadedPhoto, + // so force the extension to be .jpg anyway. It doesn't matter, + // because the filename from inputFile is not used anywhere. + photoFilename += u".jpg"_q; + } + const auto md5 = entry.file->filemd5; + const auto file = MTP_inputFile( + MTP_long(entry.file->id), + MTP_int(entry.parts->size()), + MTP_string(photoFilename), + MTP_bytes(md5)); + _photoReady.fire({ + .fullId = entry.itemId, + .info = { + .file = file, + .attachedStickers = attachedStickers, + }, + .options = options, + .edit = edit, + }); + } else if (entry.file->type == SendMediaType::File + || entry.file->type == SendMediaType::ThemeFile + || entry.file->type == SendMediaType::Audio) { + QByteArray docMd5(32, Qt::Uninitialized); + hashMd5Hex(entry.md5Hash.result(), docMd5.data()); + + const auto file = (entry.docSize > kUseBigFilesFrom) + ? MTP_inputFileBig( + MTP_long(entry.file->id), + MTP_int(entry.docPartsCount), + MTP_string(entry.file->filename)) + : MTP_inputFile( + MTP_long(entry.file->id), + MTP_int(entry.docPartsCount), + MTP_string(entry.file->filename), + MTP_bytes(docMd5)); + const auto thumb = [&]() -> std::optional { + if (entry.parts->empty()) { + return std::nullopt; + } + const auto thumbFilename = entry.file->thumbname; + const auto thumbMd5 = entry.file->thumbmd5; + return MTP_inputFile( + MTP_long(entry.file->thumbId), + MTP_int(entry.parts->size()), + MTP_string(thumbFilename), + MTP_bytes(thumbMd5)); + }(); + _documentReady.fire({ + .fullId = entry.itemId, + .info = { + .file = file, + .thumb = thumb, + .attachedStickers = attachedStickers, + }, + .options = options, + .edit = edit, + }); + } else if (entry.file->type == SendMediaType::Secure) { + _secureReady.fire({ + entry.itemId, + entry.file->id, + int(entry.parts->size()), + }); + } } void Uploader::partFailed(const MTP::Error &error, mtpRequestId requestId) { - // failed to upload current file - _nonPremiumDelayed.remove(requestId); - if ((requestsSent.find(requestId) != requestsSent.cend()) - || (docRequestsSent.find(requestId) != docRequestsSent.cend())) { - currentFailed(); - } - sendNext(); + const auto request = finishRequest(requestId); + failed(request.itemId); } } // namespace Storage diff --git a/Telegram/SourceFiles/storage/file_upload.h b/Telegram/SourceFiles/storage/file_upload.h index 9f72fbfa5..6f87f6a56 100644 --- a/Telegram/SourceFiles/storage/file_upload.h +++ b/Telegram/SourceFiles/storage/file_upload.h @@ -9,11 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_common.h" #include "base/timer.h" +#include "base/weak_ptr.h" #include "mtproto/facade.h" class ApiWrap; -struct FileLoadResult; -struct SendMediaReady; +struct FilePrepareResult; namespace Api { enum class SendProgressType; @@ -47,54 +47,48 @@ struct UploadSecureDone { int partsCount = 0; }; -class Uploader final : public QObject { +class Uploader final : public base::has_weak_ptr { public: explicit Uploader(not_null api); ~Uploader(); [[nodiscard]] Main::Session &session() const; + [[nodiscard]] FullMsgId currentUploadId() const; - [[nodiscard]] FullMsgId currentUploadId() const { - return uploadingId; - } - - void uploadMedia(const FullMsgId &msgId, const SendMediaReady &image); void upload( - const FullMsgId &msgId, - const std::shared_ptr &file); - - void cancel(const FullMsgId &msgId); - void pause(const FullMsgId &msgId); - void confirm(const FullMsgId &msgId); + FullMsgId itemId, + const std::shared_ptr &file); + void pause(FullMsgId itemId); + void cancel(FullMsgId itemId); void cancelAll(); - void clear(); - rpl::producer photoReady() const { + [[nodiscard]] rpl::producer photoReady() const { return _photoReady.events(); } - rpl::producer documentReady() const { + [[nodiscard]] rpl::producer documentReady() const { return _documentReady.events(); } - rpl::producer secureReady() const { + [[nodiscard]] rpl::producer secureReady() const { return _secureReady.events(); } - rpl::producer photoProgress() const { + [[nodiscard]] rpl::producer photoProgress() const { return _photoProgress.events(); } - rpl::producer documentProgress() const { + [[nodiscard]] rpl::producer documentProgress() const { return _documentProgress.events(); } - rpl::producer secureProgress() const { + [[nodiscard]] auto secureProgress() const + -> rpl::producer { return _secureProgress.events(); } - rpl::producer photoFailed() const { + [[nodiscard]] rpl::producer photoFailed() const { return _photoFailed.events(); } - rpl::producer documentFailed() const { + [[nodiscard]] rpl::producer documentFailed() const { return _documentFailed.events(); } - rpl::producer secureFailed() const { + [[nodiscard]] rpl::producer secureFailed() const { return _secureFailed.events(); } @@ -103,23 +97,53 @@ public: } void unpause(); - void sendNext(); void stopSessions(); private: - struct File; + struct Entry; + struct Request; + + enum class SendResult : uchar { + Success, + Failed, + DcIndexFull, + }; + + void maybeSend(); + [[nodiscard]] bool canAddDcIndex() const; + [[nodiscard]] std::optional chooseDcIndexForNextRequest( + const base::flat_set &used); + [[nodiscard]] Entry *chooseEntryForNextRequest(); + [[nodiscard]] SendResult sendPart(not_null entry, uchar dcIndex); + [[nodiscard]] auto sendPendingPart(not_null entry, uchar dcIndex) + -> SendResult; + [[nodiscard]] auto sendDocPart(not_null entry, uchar dcIndex) + -> SendResult; + [[nodiscard]] auto sendSlicedPart(not_null entry, uchar dcIndex) + -> SendResult; + [[nodiscard]] QByteArray readDocPart(not_null entry); + void removeDcIndex(); + + template + void sendPreparedRequest(Prepared &&prepared, Request &&request); + + void maybeFinishFront(); + void finishFront(); void partLoaded(const MTPBool &result, mtpRequestId requestId); void partFailed(const MTP::Error &error, mtpRequestId requestId); + Request finishRequest(mtpRequestId requestId); - void processPhotoProgress(const FullMsgId &msgId); - void processPhotoFailed(const FullMsgId &msgId); - void processDocumentProgress(const FullMsgId &msgId); - void processDocumentFailed(const FullMsgId &msgId); + void processPhotoProgress(FullMsgId itemId); + void processPhotoFailed(FullMsgId itemId); + void processDocumentProgress(FullMsgId itemId); + void processDocumentFailed(FullMsgId itemId); - void notifyFailed(FullMsgId id, const File &file); - void currentFailed(); - void cancelRequests(); + void notifyFailed(const Entry &entry); + void failed(FullMsgId itemId); + void cancelRequests(FullMsgId itemId); + void cancelAllRequests(); + void clear(); void sendProgressUpdate( not_null item, @@ -127,16 +151,19 @@ private: int progress = 0); const not_null _api; - base::flat_map requestsSent; - base::flat_map docRequestsSent; - base::flat_map dcMap; - base::flat_set _nonPremiumDelayed; - uint32 sentSize = 0; // FileSize: Right now any file size fits 32 bit. - uint32 sentSizes[MTP::kUploadSessionsCountMax] = { 0 }; - FullMsgId uploadingId; + std::vector _queue; + + base::flat_map _requests; + std::vector _sentPerDcIndex; + + // Fast requests since the latest dc index addition. + base::flat_set _dcIndicesWithFastRequests; + crl::time _latestDcIndexAdded = 0; + crl::time _latestDcIndexRemoved = 0; + std::vector _pendingFromRemovedDcIndices; + FullMsgId _pausedId; - std::map queue; base::Timer _nextTimer, _stopSessionsTimer; rpl::event_stream _photoReady; diff --git a/Telegram/SourceFiles/storage/localimageloader.cpp b/Telegram/SourceFiles/storage/localimageloader.cpp index 6fa1e72b8..cca4ee92b 100644 --- a/Telegram/SourceFiles/storage/localimageloader.cpp +++ b/Telegram/SourceFiles/storage/localimageloader.cpp @@ -223,42 +223,6 @@ int PhotoSideLimit() { return PhotoSideLimit(SendLargePhotos.value()); } -SendMediaReady::SendMediaReady( - SendMediaType type, - const QString &file, - const QString &filename, - int64 filesize, - const QByteArray &data, - const uint64 &id, - const uint64 &thumbId, - const QString &thumbExt, - const PeerId &peer, - const MTPPhoto &photo, - const PreparedPhotoThumbs &photoThumbs, - const MTPDocument &document, - const QByteArray &jpeg) -: type(type) -, file(file) -, filename(filename) -, filesize(filesize) -, data(data) -, thumbExt(thumbExt) -, id(id) -, thumbId(thumbId) -, peer(peer) -, photo(photo) -, document(document) -, photoThumbs(photoThumbs) { - if (!jpeg.isEmpty()) { - int32 size = jpeg.size(); - for (int32 i = 0, part = 0; i < size; i += kPhotoUploadPartSize, ++part) { - parts.insert(part, jpeg.mid(i, kPhotoUploadPartSize)); - } - jpeg_md5.resize(32); - hashMd5Hex(jpeg.constData(), jpeg.size(), jpeg_md5.data()); - } -} - TaskQueue::TaskQueue(crl::time stopTimeoutMs) { if (stopTimeoutMs > 0) { _stopTimer = new QTimer(this); @@ -456,46 +420,50 @@ SendingAlbum::Item::Item(TaskId taskId) : taskId(taskId) { } -FileLoadResult::FileLoadResult( - TaskId taskId, - uint64 id, - const FileLoadTo &to, - const TextWithTags &caption, - bool spoiler, - std::shared_ptr album) -: taskId(taskId) -, id(id) -, to(to) -, album(std::move(album)) -, caption(caption) -, spoiler(spoiler) { +FilePrepareResult::FilePrepareResult(FilePrepareDescriptor &&descriptor) +: taskId(descriptor.taskId) +, id(descriptor.id) +, to(std::move(descriptor.to)) +, album(std::move(descriptor.album)) +, type(descriptor.type) +, caption(std::move(descriptor.caption)) +, spoiler(descriptor.spoiler) { } -void FileLoadResult::setFileData(const QByteArray &filedata) { +void FilePrepareResult::setFileData(const QByteArray &filedata) { if (filedata.isEmpty()) { partssize = 0; } else { partssize = filedata.size(); + fileparts.reserve( + (partssize + kPhotoUploadPartSize - 1) / kPhotoUploadPartSize); for (int32 i = 0, part = 0; i < partssize; i += kPhotoUploadPartSize, ++part) { - fileparts.insert(part, filedata.mid(i, kPhotoUploadPartSize)); + fileparts.push_back(filedata.mid(i, kPhotoUploadPartSize)); } filemd5.resize(32); hashMd5Hex(filedata.constData(), filedata.size(), filemd5.data()); } } -void FileLoadResult::setThumbData(const QByteArray &thumbdata) { +void FilePrepareResult::setThumbData(const QByteArray &thumbdata) { if (!thumbdata.isEmpty()) { thumbbytes = thumbdata; int32 size = thumbdata.size(); + thumbparts.reserve( + (size + kPhotoUploadPartSize - 1) / kPhotoUploadPartSize); for (int32 i = 0, part = 0; i < size; i += kPhotoUploadPartSize, ++part) { - thumbparts.insert(part, thumbdata.mid(i, kPhotoUploadPartSize)); + thumbparts.push_back(thumbdata.mid(i, kPhotoUploadPartSize)); } thumbmd5.resize(32); hashMd5Hex(thumbdata.constData(), thumbdata.size(), thumbmd5.data()); } } +std::shared_ptr MakePreparedFile( + FilePrepareDescriptor &&descriptor) { + return std::make_shared(std::move(descriptor)); +} + FileLoadTask::FileLoadTask( not_null session, const QString &filepath, @@ -711,13 +679,14 @@ bool FileLoadTask::FillImageInformation( } void FileLoadTask::process(Args &&args) { - _result = std::make_shared( - id(), - _id, - _to, - _caption, - _spoiler, - _album); + _result = MakePreparedFile({ + .taskId = id(), + .id = _id, + .to = _to, + .caption = _caption, + .spoiler = _spoiler, + .album = _album, + }); QString filename, filemime; qint64 filesize = 0; @@ -1063,7 +1032,7 @@ void FileLoadTask::finish() { } } -FileLoadResult *FileLoadTask::peekResult() const { +FilePrepareResult *FileLoadTask::peekResult() const { return _result.get(); } diff --git a/Telegram/SourceFiles/storage/localimageloader.h b/Telegram/SourceFiles/storage/localimageloader.h index d2e64b536..2756ac244 100644 --- a/Telegram/SourceFiles/storage/localimageloader.h +++ b/Telegram/SourceFiles/storage/localimageloader.h @@ -36,43 +36,8 @@ enum class SendMediaType { Secure, }; -using UploadFileParts = QMap; -struct SendMediaReady { - SendMediaReady() = default; // temp - SendMediaReady( - SendMediaType type, - const QString &file, - const QString &filename, - int64 filesize, - const QByteArray &data, - const uint64 &id, - const uint64 &thumbId, - const QString &thumbExt, - const PeerId &peer, - const MTPPhoto &photo, - const PreparedPhotoThumbs &photoThumbs, - const MTPDocument &document, - const QByteArray &jpeg); - - SendMediaType type; - QString file, filename; - int64 filesize = 0; - QByteArray data; - QString thumbExt; - uint64 id, thumbId; // id always file-id of media, thumbId is file-id of thumb ( == id for photos) - PeerId peer; - - MTPPhoto photo; - MTPDocument document; - PreparedPhotoThumbs photoThumbs; - UploadFileParts parts; - QByteArray jpeg_md5; - - QString caption; - -}; - using TaskId = void*; // no interface, just id +inline constexpr auto kEmptyTaskId = TaskId(); class Task { public: @@ -144,7 +109,7 @@ struct SendingAlbum { struct Item { explicit Item(TaskId taskId); - TaskId taskId; + TaskId taskId = kEmptyTaskId; uint64 randomId = 0; FullMsgId msgId; std::optional media; @@ -182,17 +147,21 @@ struct FileLoadTo { MsgId replaceMediaOf; }; -struct FileLoadResult { - FileLoadResult( - TaskId taskId, - uint64 id, - const FileLoadTo &to, - const TextWithTags &caption, - bool spoiler, - std::shared_ptr album); +using UploadFileParts = std::vector; +struct FilePrepareDescriptor { + TaskId taskId = kEmptyTaskId; + base::required id; + SendMediaType type = SendMediaType::File; + FileLoadTo to = { PeerId(), Api::SendOptions(), FullReplyTo(), MsgId() }; + TextWithTags caption; + bool spoiler = false; + std::shared_ptr album; +}; +struct FilePrepareResult { + explicit FilePrepareResult(FilePrepareDescriptor &&descriptor); - TaskId taskId; - uint64 id; + TaskId taskId = kEmptyTaskId; + uint64 id = 0; FileLoadTo to; std::shared_ptr album; SendMediaType type = SendMediaType::File; @@ -216,8 +185,8 @@ struct FileLoadResult { QImage goodThumbnail; QByteArray goodThumbnailBytes; - MTPPhoto photo; - MTPDocument document; + MTPPhoto photo = MTP_photoEmpty(MTP_long(0)); + MTPDocument document = MTP_documentEmpty(MTP_long(0)); PreparedPhotoThumbs photoThumbs; TextWithTags caption; @@ -230,6 +199,9 @@ struct FileLoadResult { }; +[[nodiscard]] std::shared_ptr MakePreparedFile( + FilePrepareDescriptor &&descriptor); + class FileLoadTask final : public Task { public: static std::unique_ptr ReadMediaInformation( @@ -276,7 +248,7 @@ public: } void finish() override; - FileLoadResult *peekResult() const; + FilePrepareResult *peekResult() const; private: static bool CheckForSong( @@ -312,6 +284,6 @@ private: TextWithTags _caption; bool _spoiler = false; - std::shared_ptr _result; + std::shared_ptr _result; }; diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index 008726ee6..691d84f16 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -397,7 +397,9 @@ void BoostBox( close->parentWidget(), MakeTitle( box, - rpl::duplicate(title), + (data.group + ? tr::lng_boost_group_button + : tr::lng_boost_channel_button)(), rpl::duplicate(repeated), false)); const auto titleInner = faded.data(); diff --git a/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp b/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp new file mode 100644 index 000000000..4498becad --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp @@ -0,0 +1,271 @@ +/* +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 "ui/boxes/collectible_info_box.h" + +#include "base/unixtime.h" +#include "core/file_utilities.h" +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "info/channel_statistics/earn/earn_format.h" +#include "ui/layers/generic_box.h" +#include "ui/text/format_values.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "ui/dynamic_image.h" +#include "ui/painter.h" +#include "settings/settings_common.h" +#include "styles/style_boxes.h" +#include "styles/style_layers.h" + +#include +#include + +namespace Ui { +namespace { + +constexpr auto kTonMultiplier = uint64(1000000000); + +[[nodiscard]] QString FormatEntity(CollectibleType type, QString entity) { + switch (type) { + case CollectibleType::Phone: { + static const auto kNonDigits = QRegularExpression(u"[^\\d]"_q); + entity.replace(kNonDigits, QString()); + } return Ui::FormatPhone(entity); + case CollectibleType::Username: + return entity.startsWith('@') ? entity : ('@' + entity); + } + Unexpected("CollectibleType in FormatEntity."); +} + +[[nodiscard]] QString FormatDate(TimeId date) { + return langDateTime(base::unixtime::parse(date)); +} + +[[nodiscard]] TextWithEntities FormatPrice( + const CollectibleInfo &info, + const CollectibleDetails &details) { + auto minor = Info::ChannelEarn::MinorPart(info.cryptoAmount); + if (minor.size() == 1 && minor.at(0) == '.') { + minor += '0'; + } + auto price = (info.cryptoCurrency == u"TON"_q) + ? base::duplicate( + details.tonEmoji + ).append( + Info::ChannelEarn::MajorPart(info.cryptoAmount) + ).append(minor) + : TextWithEntities{ ('{' + + info.cryptoCurrency + ':' + QString::number(info.cryptoAmount) + + '}') }; + const auto fiat = Ui::FillAmountAndCurrency(info.amount, info.currency); + return Ui::Text::Wrapped( + price, + EntityType::Bold + ).append(u" ("_q + fiat + ')'); +} + +[[nodiscard]] object_ptr MakeOwnerCell( + not_null parent, + const CollectibleInfo &info) { + const auto st = &st::defaultMultiSelectItem; + const auto size = st->height; + auto result = object_ptr(parent.get(), size); + const auto raw = result.data(); + + const auto name = info.ownerName; + const auto userpic = info.ownerUserpic; + const auto nameWidth = st->style.font->width(name); + const auto added = size + st->padding.left() + st->padding.right(); + const auto subscribed = std::make_shared(false); + raw->paintRequest() | rpl::start_with_next([=] { + const auto use = std::min(nameWidth + added, raw->width()); + const auto x = (raw->width() - use) / 2; + if (const auto available = use - added; available > 0) { + auto p = QPainter(raw); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st->textBg); + p.drawRoundedRect(x, 0, use, size, size / 2., size / 2.); + + if (!*subscribed) { + *subscribed = true; + userpic->subscribeToUpdates([=] { raw->update(); }); + } + p.drawImage(QRect(x, 0, size, size), userpic->image(size)); + + const auto textx = x + size + st->padding.left(); + const auto texty = st->padding.top() + st->style.font->ascent; + const auto text = (use == nameWidth + added) + ? name + : st->style.font->elided(name, available); + p.setPen(st->textFg); + p.setFont(st->style.font); + p.drawText(textx, texty, text); + } + }, raw->lifetime()); + + return result; +} + +} // namespace + +CollectibleType DetectCollectibleType(const QString &entity) { + return entity.startsWith('+') + ? CollectibleType::Phone + : CollectibleType::Username; +} + +void CollectibleInfoBox( + not_null box, + CollectibleInfo info, + CollectibleDetails details) { + box->setWidth(st::boxWideWidth); + box->setStyle(st::collectibleBox); + + const auto type = DetectCollectibleType(info.entity); + + const auto icon = box->addRow( + object_ptr(box, st::collectibleIconDiameter), + st::collectibleIconPadding); + icon->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + const auto size = icon->height(); + const auto inner = QRect( + (icon->width() - size) / 2, + 0, + size, + size); + if (!inner.intersects(clip)) { + return; + } + auto p = QPainter(icon); + auto hq = PainterHighQualityEnabler(p); + p.setBrush(st::defaultActiveButton.textBg); + p.setPen(Qt::NoPen); + p.drawEllipse(inner); + }, icon->lifetime()); + const auto lottieSize = st::collectibleIcon; + auto lottie = Settings::CreateLottieIcon( + icon, + { + .name = (type == CollectibleType::Phone + ? u"collectible_phone"_q + : u"collectible_username"_q), + .color = &st::defaultActiveButton.textFg, + .sizeOverride = { lottieSize, lottieSize }, + }, + QMargins()); + box->showFinishes( + ) | rpl::start_with_next([animate = std::move(lottie.animate)] { + animate(anim::repeat::once); + }, box->lifetime()); + const auto animation = lottie.widget.release(); + icon->sizeValue() | rpl::start_with_next([=](QSize size) { + const auto skip = (type == CollectibleType::Phone) + ? style::ConvertScale(2) + : 0; + animation->move( + (size.width() - animation->width()) / 2, + skip + (size.height() - animation->height()) / 2); + }, animation->lifetime()); + + const auto formatted = FormatEntity(type, info.entity); + const auto header = (type == CollectibleType::Phone) + ? tr::lng_collectible_phone_title( + tr::now, + lt_phone, + Ui::Text::Link(formatted), + Ui::Text::WithEntities) + : tr::lng_collectible_username_title( + tr::now, + lt_username, + Ui::Text::Link(formatted), + Ui::Text::WithEntities); + const auto copyCallback = [box, type, formatted, text = info.copyText] { + QGuiApplication::clipboard()->setText( + text.isEmpty() ? formatted : text); + box->uiShow()->showToast((type == CollectibleType::Phone) + ? tr::lng_text_copied(tr::now) + : tr::lng_username_copied(tr::now)); + }; + box->addRow( + object_ptr( + box, + rpl::single(header), + st::collectibleHeader), + st::collectibleHeaderPadding + )->setClickHandlerFilter([copyCallback](const auto &...) { + copyCallback(); + return false; + }); + + box->addRow(MakeOwnerCell(box, info), st::collectibleOwnerPadding); + + const auto text = ((type == CollectibleType::Phone) + ? tr::lng_collectible_phone_info + : tr::lng_collectible_username_info)( + tr::now, + lt_date, + TextWithEntities{ FormatDate(info.date) }, + lt_price, + FormatPrice(info, details), + Ui::Text::RichLangValue); + const auto label = box->addRow( + object_ptr(box, st::collectibleInfo), + st::collectibleInfoPadding); + label->setAttribute(Qt::WA_TransparentForMouseEvents); + label->setMarkedText(text, details.tonEmojiContext()); + + const auto more = box->addRow( + object_ptr( + box, + tr::lng_collectible_learn_more(), + st::collectibleMore), + st::collectibleMorePadding); + more->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + more->setClickedCallback([url = info.url] { + File::OpenUrl(url); + }); + + const auto phrase = (type == CollectibleType::Phone) + ? tr::lng_collectible_phone_copy + : tr::lng_collectible_username_copy; + auto owned = object_ptr( + box, + phrase(), + st::collectibleCopy); + const auto copy = owned.data(); + copy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + copy->setClickedCallback(copyCallback); + box->addButton(std::move(owned)); + + box->setNoContentMargin(true); + const auto buttonsParent = box->verticalLayout().get(); + const auto close = Ui::CreateChild( + buttonsParent, + st::boxTitleClose); + close->setClickedCallback([=] { + box->closeBox(); + }); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + close->moveToRight(0, 0); + }, box->lifetime()); + + box->widthValue() | rpl::start_with_next([=](int width) { + more->setFullWidth(width + - st::collectibleMorePadding.left() + - st::collectibleMorePadding.right()); + copy->setFullWidth(width + - st::collectibleBox.buttonPadding.left() + - st::collectibleBox.buttonPadding.right()); + }, box->lifetime()); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/boxes/collectible_info_box.h b/Telegram/SourceFiles/ui/boxes/collectible_info_box.h new file mode 100644 index 000000000..8b80b9650 --- /dev/null +++ b/Telegram/SourceFiles/ui/boxes/collectible_info_box.h @@ -0,0 +1,45 @@ +/* +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 +*/ +#pragma once + +namespace Ui { + +class GenericBox; +class DynamicImage; + +enum class CollectibleType { + Phone, + Username, +}; + +[[nodiscard]] CollectibleType DetectCollectibleType(const QString &entity); + +struct CollectibleInfo { + QString entity; + QString copyText; + std::shared_ptr ownerUserpic; + QString ownerName; + uint64 cryptoAmount = 0; + uint64 amount = 0; + QString cryptoCurrency; + QString currency; + QString url; + TimeId date = 0; +}; + +struct CollectibleDetails { + TextWithEntities tonEmoji; + Fn tonEmojiContext; +}; + +void CollectibleInfoBox( + not_null box, + CollectibleInfo info, + CollectibleDetails details); + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 42eac21de..262c852d5 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -800,17 +800,20 @@ void Panel::openExternalLink(const QJsonObject &args) { _delegate->botClose(); return; } + const auto iv = args["try_instant_view"].toBool(); const auto url = args["url"].toString(); const auto lower = url.toLower(); - if (url.isEmpty() - || (!lower.startsWith("http://") && !lower.startsWith("https://"))) { - LOG(("BotWebView Error: Bad 'url' in openExternalLink.")); + if (!lower.startsWith("http://") && !lower.startsWith("https://")) { + LOG(("BotWebView Error: Bad url in openExternalLink: %1").arg(url)); _delegate->botClose(); return; } else if (!allowOpenLink()) { return; + } else if (iv) { + _delegate->botOpenIvLink(url); + } else { + File::OpenUrl(url); } - File::OpenUrl(url); } void Panel::openInvoice(const QJsonObject &args) { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index 9c4be46d0..bb22a4dfc 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -57,6 +57,7 @@ public: virtual bool botHandleLocalUri(QString uri, bool keepOpen) = 0; virtual void botHandleInvoice(QString slug) = 0; virtual void botHandleMenuButton(MenuButton button) = 0; + virtual void botOpenIvLink(QString uri) = 0; virtual void botSendData(QByteArray data) = 0; virtual void botSwitchInlineQuery( std::vector chatTypes, diff --git a/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.cpp b/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.cpp index 84d98f521..552f46412 100644 --- a/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.cpp +++ b/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.cpp @@ -39,7 +39,7 @@ public: Content( not_null parent, not_null controller, - const MessageSendingAnimationFrom &fromInfo, + MessageSendingAnimationFrom &&fromInfo, MessageSendingAnimationController::SendingInfoTo &&to); [[nodiscard]] rpl::producer<> destroyRequests() const; @@ -79,7 +79,7 @@ private: Content::Content( not_null parent, not_null controller, - const MessageSendingAnimationFrom &fromInfo, + MessageSendingAnimationFrom &&fromInfo, MessageSendingAnimationController::SendingInfoTo &&to) : RpWidget(parent) , _controller(controller) @@ -98,8 +98,12 @@ Content::Content( base::take( _toInfo.globalEndTopLeft ) | rpl::distinct_until_changed( - ) | rpl::start_with_next([=](const QPoint &p) { - _to = parent->mapFromGlobal(p); + ) | rpl::start_with_next([=](const std::optional &p) { + if (p) { + _to = parent->mapFromGlobal(*p); + } else { + _destroyRequests.fire({}); + } }, lifetime()); _controller->session().downloaderTaskFinished( @@ -366,6 +370,20 @@ void Content::createBubble() { MessageSendingAnimationController::MessageSendingAnimationController( not_null controller) : _controller(controller) { + subscribeToDestructions(); +} + +void MessageSendingAnimationController::subscribeToDestructions() { + _controller->session().data().itemIdChanged( + ) | rpl::start_with_next([=](Data::Session::IdChange change) { + _itemSendPending.remove(change.oldId); + }, _lifetime); + + _controller->session().data().itemRemoved( + ) | rpl::start_with_next([=](not_null item) { + _itemSendPending.remove(item->id); + _processing.remove(item); + }, _lifetime); } void MessageSendingAnimationController::appendSending( @@ -389,26 +407,23 @@ void MessageSendingAnimationController::startAnimation(SendingInfoTo &&to) { if (it == end(_itemSendPending)) { return; } - const auto msg = it->first; + auto from = std::move(it->second); + _itemSendPending.erase(it); auto content = base::make_unique_q( container, _controller, - it->second, + std::move(from), std::move(to)); + content->destroyRequests( ) | rpl::start_with_next([=] { - _itemSendPending.erase(msg); _processing.erase(item); }, content->lifetime()); _processing.emplace(item, std::move(content)); } -bool MessageSendingAnimationController::hasLocalMessage(MsgId msgId) const { - return _itemSendPending.contains(msgId); -} - bool MessageSendingAnimationController::hasAnimatedMessage( not_null item) const { return _processing.contains(item); @@ -421,7 +436,7 @@ void MessageSendingAnimationController::clear() { bool MessageSendingAnimationController::checkExpectedType( not_null item) { - const auto it = _itemSendPending.find(item->fullId().msg); + const auto it = _itemSendPending.find(item->id); if (it == end(_itemSendPending)) { return false; } diff --git a/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.h b/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.h index 92bb9a915..8250e778a 100644 --- a/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.h +++ b/Telegram/SourceFiles/ui/effects/message_sending_animation_controller.h @@ -29,7 +29,7 @@ public: not_null controller); struct SendingInfoTo { - rpl::producer globalEndTopLeft; + rpl::producer> globalEndTopLeft; Fn view; Fn paintContext; }; @@ -37,13 +37,13 @@ public: void appendSending(MessageSendingAnimationFrom from); void startAnimation(SendingInfoTo &&to); - [[nodiscard]] bool hasLocalMessage(MsgId msgId) const; [[nodiscard]] bool hasAnimatedMessage(not_null item) const; [[nodiscard]] bool checkExpectedType(not_null item); void clear(); private: + void subscribeToDestructions(); const not_null _controller; base::flat_map _itemSendPending; @@ -52,6 +52,8 @@ private: not_null, base::unique_qptr> _processing; + rpl::lifetime _lifetime; + }; } // namespace Ui diff --git a/Telegram/SourceFiles/ui/text/format_values.cpp b/Telegram/SourceFiles/ui/text/format_values.cpp index dec5eb312..41e985723 100644 --- a/Telegram/SourceFiles/ui/text/format_values.cpp +++ b/Telegram/SourceFiles/ui/text/format_values.cpp @@ -357,6 +357,7 @@ CurrencyRule LookupCurrencyRule(const QString ¤cy) { char do_decimal_point() const override { return decimal; } char do_thousands_sep() const override { return thousands; } + std::string do_grouping() const override { return "\3"; } char decimal = '.'; char thousands = ','; diff --git a/Telegram/SourceFiles/window/themes/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp index 7409a95ee..b225b321e 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme.cpp @@ -585,11 +585,11 @@ void ChatBackground::checkUploadWallPaper() { } const auto ready = PrepareWallPaper(_session->mainDcId(), _original); - const auto documentId = ready.id; + const auto documentId = ready->id; _wallPaperUploadId = FullMsgId( _session->userPeerId(), _session->data().nextLocalMessageId()); - _session->uploader().uploadMedia(_wallPaperUploadId, ready); + _session->uploader().upload(_wallPaperUploadId, ready); if (_wallPaperUploadLifetime) { return; } @@ -1529,7 +1529,9 @@ bool ReadPaletteValues(const QByteArray &content, Fn PrepareWallPaper( + MTP::DcId dcId, + const QImage &image) { PreparedPhotoThumbs thumbnails; QVector sizes; @@ -1555,6 +1557,7 @@ SendMediaReady PrepareWallPaper(MTP::DcId dcId, const QImage &image) { }; push("s", scaled(320)); + const auto id = base::RandomValue(); const auto filename = u"wallpaper.jpg"_q; auto attributes = QVector( 1, @@ -1562,8 +1565,20 @@ SendMediaReady PrepareWallPaper(MTP::DcId dcId, const QImage &image) { attributes.push_back(MTP_documentAttributeImageSize( MTP_int(image.width()), MTP_int(image.height()))); - const auto id = base::RandomValue(); - const auto document = MTP_document( + + auto result = MakePreparedFile({ + .id = id, + .type = SendMediaType::ThemeFile, + }); + result->filename = filename; + result->content = jpeg; + result->filesize = jpeg.size(); + result->setFileData(jpeg); + if (thumbnails.empty()) { + result->thumb = thumbnails.front().second.image; + result->thumbbytes = thumbnails.front().second.bytes; + } + result->document = MTP_document( MTP_flags(0), MTP_long(id), MTP_long(0), @@ -1575,21 +1590,7 @@ SendMediaReady PrepareWallPaper(MTP::DcId dcId, const QImage &image) { MTPVector(), MTP_int(dcId), MTP_vector(attributes)); - - return SendMediaReady( - SendMediaType::ThemeFile, - QString(), // filepath - filename, - jpeg.size(), - jpeg, - id, - 0, - QString(), - PeerId(), - MTP_photoEmpty(MTP_long(0)), - thumbnails, - document, - QByteArray()); + return result; } std::unique_ptr DefaultChatThemeOn(rpl::lifetime &lifetime) { diff --git a/Telegram/SourceFiles/window/themes/window_theme.h b/Telegram/SourceFiles/window/themes/window_theme.h index 1425e4d07..b0497fb0c 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.h +++ b/Telegram/SourceFiles/window/themes/window_theme.h @@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/style/style_core_palette.h" class QFileSystemWatcher; -struct SendMediaReady; +struct FilePrepareResult; namespace style { struct colorizer; @@ -298,7 +298,7 @@ private: }; -[[nodiscard]] SendMediaReady PrepareWallPaper( +[[nodiscard]] std::shared_ptr PrepareWallPaper( MTP::DcId dcId, const QImage &image); diff --git a/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp b/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp index d448ca185..c430b3e28 100644 --- a/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp @@ -385,7 +385,7 @@ bool CopyColorsToPalette( }); } -SendMediaReady PrepareThemeMedia( +std::shared_ptr PrepareThemeMedia( MTP::DcId dcId, const QString &name, const QByteArray &content) { @@ -403,28 +403,29 @@ SendMediaReady PrepareThemeMedia( thumbnail.save(&buffer, "JPG", 87); } - const auto push = [&]( - const char *type, - QImage &&image, - QByteArray bytes = QByteArray()) { - sizes.push_back(MTP_photoSize( - MTP_string(type), - MTP_int(image.width()), - MTP_int(image.height()), MTP_int(0))); - thumbnails.emplace(type[0], PreparedPhotoThumb{ - .image = std::move(image), - .bytes = std::move(bytes) - }); - }; - push("s", std::move(thumbnail), thumbnailBytes); + sizes.push_back(MTP_photoSize( + MTP_string("s"), + MTP_int(thumbnail.width()), + MTP_int(thumbnail.height()), MTP_int(0))); + const auto id = base::RandomValue(); const auto filename = base::FileNameFromUserString(name) + u".tdesktop-theme"_q; auto attributes = QVector( 1, MTP_documentAttributeFilename(MTP_string(filename))); - const auto id = base::RandomValue(); - const auto document = MTP_document( + + auto result = MakePreparedFile({ + .id = id, + .type = SendMediaType::ThemeFile, + }); + result->filename = filename; + result->content = content; + result->filesize = content.size(); + result->thumb = thumbnail; + result->thumbname = "thumb.jpg"; + result->setThumbData(thumbnailBytes); + result->document = MTP_document( MTP_flags(0), MTP_long(id), MTP_long(0), @@ -436,21 +437,7 @@ SendMediaReady PrepareThemeMedia( MTPVector(), MTP_int(dcId), MTP_vector(attributes)); - - return SendMediaReady( - SendMediaType::ThemeFile, - QString(), // filepath - filename, - content.size(), - content, - id, - 0, - QString(), - PeerId(), - MTP_photoEmpty(MTP_long(0)), - thumbnails, - document, - thumbnailBytes); + return result; } Fn SavePreparedTheme( @@ -570,7 +557,7 @@ Fn SavePreparedTheme( session->mainDcId(), fields.title, theme); - state->filename = media.filename; + state->filename = media->filename; state->themeContent = theme; session->uploader().documentReady( @@ -580,7 +567,7 @@ Fn SavePreparedTheme( uploadTheme(data); }, state->lifetime); - session->uploader().uploadMedia(state->id, media); + session->uploader().upload(state->id, media); }; const auto save = [=] { @@ -999,12 +986,15 @@ ParsedTheme ParseTheme( [[nodiscard]] QString GenerateSlug() { const auto letters = uint8('Z' + 1 - 'A'); const auto digits = uint8('9' + 1 - '0'); + const auto firstValues = uint8(2 * letters); const auto values = uint8(2 * letters + digits); auto result = QString(); result.reserve(kRandomSlugSize); for (auto i = 0; i != kRandomSlugSize; ++i) { - const auto value = base::RandomValue() % values; + const auto value = i + ? (base::RandomValue() % values) + : (base::RandomValue() % firstValues); if (value < letters) { result.append(char('A' + value)); } else if (value < 2 * letters) { diff --git a/Telegram/SourceFiles/window/window_main_menu.cpp b/Telegram/SourceFiles/window/window_main_menu.cpp index 995641c8c..fa02678fb 100644 --- a/Telegram/SourceFiles/window/window_main_menu.cpp +++ b/Telegram/SourceFiles/window/window_main_menu.cpp @@ -7,66 +7,61 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "window/window_main_menu.h" -#include "window/themes/window_theme.h" -#include "window/window_peer_menu.h" -#include "window/window_session_controller.h" -#include "window/window_controller.h" +#include "apiwrap.h" +#include "base/qt_signal_producer.h" +#include "boxes/about_box.h" +#include "boxes/peer_list_controllers.h" +#include "boxes/premium_preview_box.h" +#include "calls/calls_box_controller.h" +#include "core/application.h" +#include "core/click_handler_types.h" +#include "data/data_changes.h" +#include "data/data_document_media.h" +#include "data/data_folder.h" +#include "data/data_session.h" +#include "data/data_stories.h" +#include "data/data_user.h" +#include "info/info_memento.h" +#include "info/profile/info_profile_badge.h" +#include "info/profile/info_profile_emoji_status_panel.h" +#include "info/stories/info_stories_widget.h" +#include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_domain.h" +#include "main/main_session.h" +#include "main/main_session_settings.h" +#include "mtproto/mtproto_config.h" +#include "settings/settings_advanced.h" +#include "settings/settings_calls.h" +#include "settings/settings_information.h" +#include "storage/localstorage.h" +#include "storage/storage_account.h" +#include "support/support_templates.h" +#include "ui/boxes/confirm_box.h" #include "ui/chat/chat_theme.h" #include "ui/controls/userpic_button.h" #include "ui/effects/snowflakes.h" #include "ui/effects/toggle_arrow.h" +#include "ui/painter.h" +#include "ui/text/text_options.h" +#include "ui/text/text_utilities.h" +#include "ui/unread_badge_paint.h" +#include "ui/vertical_list.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" -#include "ui/widgets/tooltip.h" #include "ui/wrap/slide_wrap.h" -#include "ui/text/text_utilities.h" -#include "ui/text/text_options.h" -#include "ui/new_badges.h" -#include "ui/painter.h" -#include "ui/vertical_list.h" -#include "ui/unread_badge_paint.h" -#include "inline_bots/bot_attach_web_view.h" -#include "storage/localstorage.h" -#include "storage/storage_account.h" -#include "support/support_templates.h" -#include "settings/settings_advanced.h" -#include "settings/settings_calls.h" -#include "settings/settings_information.h" -#include "info/profile/info_profile_badge.h" -#include "info/profile/info_profile_emoji_status_panel.h" -#include "info/stories/info_stories_widget.h" -#include "info/info_memento.h" -#include "base/platform/base_platform_info.h" -#include "base/qt_signal_producer.h" -#include "boxes/about_box.h" -#include "ui/boxes/confirm_box.h" -#include "boxes/peer_list_controllers.h" -#include "boxes/premium_preview_box.h" -#include "calls/calls_box_controller.h" -#include "lang/lang_keys.h" -#include "core/click_handler_types.h" -#include "core/application.h" -#include "main/main_session.h" -#include "main/main_session_settings.h" -#include "main/main_account.h" -#include "main/main_domain.h" -#include "mtproto/mtproto_config.h" -#include "data/data_chat.h" -#include "data/data_document_media.h" -#include "data/data_folder.h" -#include "data/data_session.h" -#include "data/data_user.h" -#include "data/data_changes.h" -#include "data/data_channel.h" -#include "data/data_stories.h" -#include "mainwidget.h" +#include "window/themes/window_theme.h" +#include "window/window_controller.h" +#include "window/window_main_menu_helpers.h" +#include "window/window_peer_menu.h" +#include "window/window_session_controller.h" #include "styles/style_chat.h" // popupMenuExpandedSeparator -#include "styles/style_window.h" -#include "styles/style_settings.h" #include "styles/style_info.h" // infoTopBarMenu #include "styles/style_layers.h" #include "styles/style_menu_icons.h" +#include "styles/style_settings.h" +#include "styles/style_window.h" #include #include @@ -90,154 +85,6 @@ namespace { constexpr auto kPlayStatusLimit = 12; -class VersionLabel final - : public Ui::FlatLabel - , public Ui::AbstractTooltipShower { -public: - using Ui::FlatLabel::FlatLabel; - - void clickHandlerActiveChanged( - const ClickHandlerPtr &action, - bool active) override { - update(); - if (active && action && !action->dragText().isEmpty()) { - Ui::Tooltip::Show(350, this); - } else { - Ui::Tooltip::Hide(); - } - } - - QString tooltipText() const override { - return u"Build date: %1."_q.arg(__DATE__); - } - - QPoint tooltipPos() const override { - return QCursor::pos(); - } - - bool tooltipWindowActive() const override { - return Ui::AppInFocus() && Ui::InFocusChain(window()); - } - -}; - -not_null AddMyChannelsBox( - not_null button, - not_null controller, - bool chats) { - button->setAcceptBoth(true); - - const auto myChannelsBox = [=](not_null box) { - box->setTitle(tr::lng_notification_channels()); - - const auto st = box->lifetime().make_state( - st::defaultUserpicButton); - st->photoSize = st::defaultPeerListItem.photoSize; - st->size = QSize(st->photoSize, st->photoSize); - - class Button final : public Ui::SettingsButton { - public: - using Ui::SettingsButton::SettingsButton; - - void setPeer(not_null p) { - const auto c = p->asChannel(); - const auto g = p->asChat(); - _text.setText(st::defaultPeerListItem.nameStyle, p->name()); - const auto count = c ? c->membersCount() : g->count; - _status.setText( - st::defaultTextStyle, - !p->userName().isEmpty() - ? ('@' + p->userName()) - : count - ? tr::lng_chat_status_subscribers( - tr::now, - lt_count, - count) - : QString()); - } - - int resizeGetHeight(int) override { - return st::defaultPeerListItem.height; - } - - void paintEvent(QPaintEvent *e) override { - Ui::SettingsButton::paintEvent(e); - auto p = Painter(this); - const auto &st = st::defaultPeerListItem; - const auto availableWidth = width() - - st::boxRowPadding.right() - - st.namePosition.x(); - p.setPen(st.nameFg); - auto context = Ui::Text::PaintContext{ - .position = st.namePosition, - .outerWidth = availableWidth, - .availableWidth = availableWidth, - .elisionLines = 1, - }; - _text.draw(p, context); - p.setPen(st.statusFg); - context.position = st.statusPosition; - _status.draw(p, context); - } - - private: - Ui::Text::String _text; - Ui::Text::String _status; - - }; - - const auto add = [&](not_null peer) { - const auto row = box->addRow( - object_ptr