diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 228366602..fa47c0a99 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2477,6 +2477,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_mediaview_copy" = "Copy"; "lng_mediaview_forward" = "Forward"; "lng_mediaview_delete" = "Delete"; +"lng_mediaview_save_to_profile" = "Save to Profile"; +"lng_mediaview_archive_story" = "Archive Story"; "lng_mediaview_photos_all" = "View all photos"; "lng_mediaview_files_all" = "View all files"; "lng_mediaview_single_photo" = "Single Photo"; @@ -3837,6 +3839,19 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_delete_one_sure" = "Are you sure you want to delete this story?"; "lng_stories_delete_sure#one" = "Are you sure you want to delete {count} story?"; "lng_stories_delete_sure#other" = "Are you sure you want to delete {count} stories?"; +"lng_stories_save_sure" = "Do you want to save this story to your profile?"; +"lng_stories_save_sure_many#one" = "Do you want to save {count} story to your profile?"; +"lng_stories_save_sure_many#other" = "Do you want to save {count} stories to your profile?"; +"lng_stories_save_done" = "This story is saved to your profile."; +"lng_stories_save_done_many#one" = "{count} story is saved to your profile."; +"lng_stories_save_done_many#other" = "{count} stories are saved to your profile."; +"lng_stories_save_done_about" = "Saved stories can be viewed by others on your profile until you remove them."; +"lng_stories_archive_sure" = "Do you want to hide this story from your profile?"; +"lng_stories_archive_sure_many#one" = "Do you want to hide {count} story from your profile?"; +"lng_stories_archive_sure_many#other" = "Do you want to hide {count} stories from your profile?"; +"lng_stories_archive_done" = "This story is hidden from your profile."; +"lng_stories_archive_done_many#one" = "{count} story is hidden from your profile."; +"lng_stories_archive_done_many#other" = "{count} stories are hidden from your profile."; "lng_stories_link_invalid" = "This link is broken or has expired."; diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index 61c41ffa3..a6db98e37 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -1275,6 +1275,63 @@ void Stories::deleteList(const std::vector &ids) { } } +void Stories::togglePinnedList( + const std::vector &ids, + bool pinned) { + auto list = QVector(); + list.reserve(ids.size()); + const auto selfId = session().userPeerId(); + for (const auto &id : ids) { + if (id.peer == selfId) { + list.push_back(MTP_int(id.story)); + } + } + if (list.empty()) { + return; + } + const auto api = &_owner->session().api(); + api->request(MTPstories_TogglePinned( + MTP_vector(list), + MTP_bool(pinned) + )).done([=](const MTPVector &result) { + auto &saved = _saved[selfId]; + const auto loaded = saved.loaded; + const auto lastId = !saved.ids.list.empty() + ? saved.ids.list.back() + : std::numeric_limits::max(); + auto dirty = false; + for (const auto &id : result.v) { + if (const auto maybeStory = lookup({ selfId, id.v })) { + const auto story = *maybeStory; + story->setPinned(pinned); + if (pinned) { + const auto add = loaded || (id.v >= lastId); + if (!add) { + dirty = true; + } else if (saved.ids.list.emplace(id.v).second) { + if (saved.total >= 0) { + ++saved.total; + } + } + } else if (saved.ids.list.remove(id.v)) { + if (saved.total > 0) { + --saved.total; + } + } else if (!loaded) { + dirty = true; + } + } else if (!loaded) { + dirty = true; + } + } + if (dirty) { + savedLoadMore(selfId); + } else { + _savedChanged.fire_copy(selfId); + } + }).send(); +} + void Stories::report( std::shared_ptr show, FullStoryId id, diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 630e8cc31..6cb415977 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -121,6 +121,8 @@ public: explicit Stories(not_null owner); ~Stories(); + static constexpr auto kPinnedToastDuration = 4 * crl::time(1000); + [[nodiscard]] Session &owner() const; [[nodiscard]] Main::Session &session() const; @@ -183,6 +185,7 @@ public: void savedLoadMore(PeerId peerId); void deleteList(const std::vector &ids); + void togglePinnedList(const std::vector &ids, bool pinned); void report( std::shared_ptr show, FullStoryId id, diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index b6958d4fa..21687e562 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -28,6 +28,25 @@ InfoPeerBadge { sizeTag: int; } +InfoTopBar { + height: pixels; + back: IconButton; + title: FlatLabel; + titlePosition: point; + bg: color; + mediaCancel: IconButton; + mediaActionsSkip: pixels; + mediaForward: IconButton; + mediaDelete: IconButton; + storiesSave: IconButton; + storiesArchive: IconButton; + search: IconButton; + searchRow: SearchFieldRow; + highlightBg: color; + highlightDuration: int; + radius: pixels; +} + infoMediaHeaderFg: windowFg; infoToggle: InfoToggle { @@ -156,6 +175,14 @@ infoTopBarDelete: IconButton(infoTopBarForward) { icon: icon {{ "info/info_media_delete", boxTitleCloseFg }}; iconOver: icon {{ "info/info_media_delete", boxTitleCloseFgOver }}; } +infoTopBarSaveStories: IconButton(infoTopBarForward) { + icon: icon {{ "menu/stories_saved", boxTitleCloseFg }}; + iconOver: icon {{ "menu/stories_saved", boxTitleCloseFgOver }}; +} +infoTopBarArchiveStories: IconButton(infoTopBarForward) { + icon: icon {{ "menu/archive", boxTitleCloseFg }}; + iconOver: icon {{ "menu/archive", boxTitleCloseFgOver }}; +} infoTopBar: InfoTopBar { height: infoTopBarHeight; back: infoTopBarBack; @@ -166,6 +193,8 @@ infoTopBar: InfoTopBar { mediaActionsSkip: 4px; mediaForward: infoTopBarForward; mediaDelete: infoTopBarDelete; + storiesSave: infoTopBarSaveStories; + storiesArchive: infoTopBarArchiveStories; search: infoTopBarSearch; searchRow: infoTopBarSearchRow; highlightBg: windowBgOver; @@ -221,6 +250,14 @@ infoLayerTopBarDelete: IconButton(infoLayerTopBarForward) { icon: icon {{ "info/info_media_delete", boxTitleCloseFg }}; iconOver: icon {{ "info/info_media_delete", boxTitleCloseFgOver }}; } +infoLayerTopBarSaveStories: IconButton(infoLayerTopBarForward) { + icon: icon {{ "menu/stories_saved", boxTitleCloseFg }}; + iconOver: icon {{ "menu/stories_saved", boxTitleCloseFgOver }}; +} +infoLayerTopBarArchiveStories: IconButton(infoLayerTopBarForward) { + icon: icon {{ "menu/archive", boxTitleCloseFg }}; + iconOver: icon {{ "menu/archive", boxTitleCloseFgOver }}; +} infoLayerTopBar: InfoTopBar(infoTopBar) { height: infoLayerTopBarHeight; back: infoLayerTopBarBack; @@ -231,6 +268,8 @@ infoLayerTopBar: InfoTopBar(infoTopBar) { mediaActionsSkip: 6px; mediaForward: infoLayerTopBarForward; mediaDelete: infoLayerTopBarDelete; + storiesSave: infoLayerTopBarSaveStories; + storiesArchive: infoLayerTopBarArchiveStories; search: infoTopBarSearch; searchRow: infoTopBarSearchRow; radius: boxRadius; diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp index 4f7af1f68..15473babc 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp @@ -172,6 +172,7 @@ void InnerWidget::createButtons() { return !content.elements.empty(); }), [] { return st::dialogsStories.height; }); + thumbs->show(); rpl::combine( recent->sizeValue(), rpl::duplicate(last) diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index ab7e2ab48..3259f0958 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -39,6 +39,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/message_sending_animation_common.h" #include "ui/effects/reaction_fly_animation.h" #include "ui/layers/box_content.h" +#include "ui/text/text_utilities.h" +#include "ui/toast/toast.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/round_rect.h" @@ -1254,8 +1256,16 @@ void Controller::deleteRequested() { return; } const auto id = story->fullId(); + const auto weak = base::make_weak(this); const auto owner = &story->owner(); const auto confirmed = [=](Fn close) { + if (const auto strong = weak.get()) { + if (const auto story = strong->story()) { + if (story->fullId() == id) { + moveFromShown(); + } + } + } owner->stories().deleteList({ id }); close(); }; @@ -1290,6 +1300,38 @@ void Controller::reportRequested() { })); } +void Controller::togglePinnedRequested(bool pinned) { + const auto story = this->story(); + if (!story || !story->peer()->isSelf()) { + return; + } + if (!pinned && v::is(_context.data)) { + moveFromShown(); + } + story->owner().stories().togglePinnedList({ story->fullId() }, pinned); + uiShow()->showToast({ + .text = (pinned + ? tr::lng_stories_save_done( + tr::now, + Ui::Text::Bold).append( + '\n').append( + tr::lng_stories_save_done_about(tr::now)) + : tr::lng_stories_archive_done( + tr::now, + Ui::Text::WithEntities)), + .st = &st::storiesActionToast, + .duration = (pinned + ? Data::Stories::kPinnedToastDuration + : Ui::Toast::kDefaultDuration), + }); +} + +void Controller::moveFromShown() { + if (!subjumpFor(1)) { + [[maybe_unused]] const auto jumped = subjumpFor(-1); + } +} + rpl::lifetime &Controller::lifetime() { return _lifetime; } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index c7137a250..87c588f37 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -91,7 +91,7 @@ struct ViewsSlice { int left = 0; }; -class Controller final { +class Controller final : public base::has_weak_ptr { public: explicit Controller(not_null delegate); ~Controller(); @@ -135,6 +135,7 @@ public: void shareRequested(); void deleteRequested(); void reportRequested(); + void togglePinnedRequested(bool pinned); [[nodiscard]] rpl::lifetime &lifetime(); @@ -171,6 +172,7 @@ private: void subjumpTo(int index); void checkWaitingFor(); + void moveFromShown(); void refreshViewsFromData(); bool sliceViewsTo(PeerId offset); diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.cpp b/Telegram/SourceFiles/media/stories/media_stories_view.cpp index 79528eb11..6a6a0dbd4 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_view.cpp @@ -83,6 +83,10 @@ void View::contentPressed(bool pressed) { _controller->contentPressed(pressed); } +void View::menuShown(bool shown) { + _controller->setMenuShown(shown); +} + void View::shareRequested() { _controller->shareRequested(); } @@ -95,6 +99,10 @@ void View::reportRequested() { _controller->reportRequested(); } +void View::togglePinnedRequested(bool pinned) { + _controller->togglePinnedRequested(pinned); +} + SiblingView View::sibling(SiblingType type) const { return _controller->sibling(type); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_view.h b/Telegram/SourceFiles/media/stories/media_stories_view.h index d2f62a0ac..24b58feed 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_view.h +++ b/Telegram/SourceFiles/media/stories/media_stories_view.h @@ -75,10 +75,12 @@ public: [[nodiscard]] bool paused() const; void togglePaused(bool paused); void contentPressed(bool pressed); + void menuShown(bool shown); void shareRequested(); void deleteRequested(); void reportRequested(); + void togglePinnedRequested(bool pinned); [[nodiscard]] rpl::lifetime &lifetime(); diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 85a2716d3..00db429d1 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -841,3 +841,6 @@ storiesReportBox: ReportBox(defaultReportBox) { personal: icon {{ "menu/personal", storiesComposeWhiteText }}; other: icon {{ "menu/report", storiesComposeWhiteText }}; } +storiesActionToast: Toast(defaultToast) { + maxWidth: 320px; +} diff --git a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp index 1c83f2d5c..12b6e0332 100644 --- a/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp +++ b/Telegram/SourceFiles/media/view/media_view_overlay_widget.cpp @@ -1408,6 +1408,19 @@ void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) { [=] { toMessage(); }, &st::mediaMenuIconShowInChat); } + if (story && story->peer()->isSelf()) { + const auto pinned = story->pinned(); + const auto text = pinned + ? tr::lng_mediaview_archive_story(tr::now) + : tr::lng_mediaview_save_to_profile(tr::now); + addAction(text, [=] { + if (_stories) { + _stories->togglePinnedRequested(!pinned); + } + }, pinned + ? &st::mediaMenuIconArchiveStory + : &st::mediaMenuIconSaveStory); + } if ((!story || story->canDownload()) && _document && !_document->filepath(true).isEmpty()) { @@ -2156,6 +2169,9 @@ void OverlayWidget::hideControls(bool force) { void OverlayWidget::dropdownHidden() { setFocus(); + if (_stories) { + _stories->menuShown(false); + } _ignoringDropdown = true; _lastMouseMovePos = _widget->mapFromGlobal(QCursor::pos()); updateOver(_lastMouseMovePos); @@ -5387,7 +5403,7 @@ void OverlayWidget::updateOverRect(Over state) { bool OverlayWidget::updateOverState(Over newState) { bool result = true; if (_over != newState) { - if (newState == Over::More && !_ignoringDropdown) { + if (!_stories && newState == Over::More && !_ignoringDropdown) { _dropdownShowTimer.callOnce(0); } else { _dropdownShowTimer.cancel(); @@ -5637,7 +5653,13 @@ bool OverlayWidget::handleContextMenu(std::optional position) { if (_menu->empty()) { _menu = nullptr; } else { + if (_stories) { + _stories->menuShown(true); + } _menu->setDestroyedCallback(crl::guard(_widget, [=] { + if (_stories) { + _stories->menuShown(false); + } activateControls(); _receiveMouse = false; InvokeQueued(_widget, [=] { receiveMouse(); }); @@ -5905,6 +5927,9 @@ void OverlayWidget::showDropdown() { _dropdown->moveToRight(0, height() - _dropdown->height()); _dropdown->showAnimated(Ui::PanelAnimation::Origin::BottomRight); _dropdown->setFocus(); + if (_stories) { + _stories->menuShown(true); + } } void OverlayWidget::handleTouchTimer() { diff --git a/Telegram/SourceFiles/settings/settings_common.cpp b/Telegram/SourceFiles/settings/settings_common.cpp index f2144d1cc..e48d6b742 100644 --- a/Telegram/SourceFiles/settings/settings_common.cpp +++ b/Telegram/SourceFiles/settings/settings_common.cpp @@ -219,6 +219,7 @@ void CreateRightLabel( const auto name = Ui::CreateChild( button.get(), st.rightLabel); + name->show(); rpl::combine( button->widthValue(), std::move(buttonText), diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 922cbaab8..3c697f316 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -126,6 +126,8 @@ mediaMenuIconDelete: icon {{ "menu/delete", mediaviewMenuFg }}; mediaMenuIconShowAll: icon {{ "menu/all_media", mediaviewMenuFg }}; mediaMenuIconProfile: icon {{ "menu/profile", mediaviewMenuFg }}; mediaMenuIconReport: icon {{ "menu/report", mediaviewMenuFg }}; +mediaMenuIconSaveStory: icon {{ "menu/stories_saved", mediaviewMenuFg }}; +mediaMenuIconArchiveStory: icon {{ "menu/archive", mediaviewMenuFg }}; menuIconDeleteAttention: icon {{ "menu/delete", menuIconAttentionColor }}; menuIconLeaveAttention: icon {{ "menu/leave", menuIconAttentionColor }}; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 08f805486..67dc933d7 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 08f80548668df2737905365b254624055642b937 +Subproject commit 67dc933d72fa5e20e6480bbff5a88a2c52d9d0d0