From d68e204fecca1d7478fce436eac136a69c08098d Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Tue, 28 May 2024 20:08:12 +0400 Subject: [PATCH 1/5] [bookmarks] implement recently deleted bookmarks and recovery support Signed-off-by: Kiryl Kaveryn --- map/bookmark_manager.cpp | 91 ++++++++++++++++++++++++++++++++++++++-- map/bookmark_manager.hpp | 15 ++++++- 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/map/bookmark_manager.cpp b/map/bookmark_manager.cpp index eae0df2cec..5952ad69dd 100644 --- a/map/bookmark_manager.cpp +++ b/map/bookmark_manager.cpp @@ -372,15 +372,53 @@ void BookmarkManager::DeleteBookmark(kml::MarkId bmId) ASSERT(IsBookmark(bmId), ()); auto const it = m_bookmarks.find(bmId); CHECK(it != m_bookmarks.end(), ()); - auto const groupId = it->second->GetGroupId(); - + auto const bookmark = it->second.get(); + auto const groupId = bookmark->GetGroupId(); if (groupId != kml::kInvalidMarkGroupId) + { + AddBookmarkToRecentlyDeleted(std::move(it->second)); DetachUserMark(bmId, groupId); - + } m_changesTracker.OnDeleteMark(bmId); m_bookmarks.erase(it); } +void BookmarkManager::AddBookmarkToRecentlyDeleted(std::unique_ptr && bookmark) +{ + m_recentlyDeletedBookmarks.emplace(bookmark.get()->GetId(), std::move(bookmark)); +} + +void BookmarkManager::RemoveBookmarkFromRecentlyDeleted(kml::MarkId bookmarkId) +{ + m_recentlyDeletedBookmarks.erase(bookmarkId); +} + +Bookmark * BookmarkManager::RecoverRecentlyDeletedBookmark(kml::MarkId bookmarkId) +{ + CHECK_THREAD_CHECKER(m_threadChecker, ()); + auto it = m_recentlyDeletedBookmarks.find(bookmarkId); + if (it == m_recentlyDeletedBookmarks.end()) + return nullptr; + + auto const recentlyDeletedBookmark = it->second.get(); + auto data = recentlyDeletedBookmark->GetData(); + + auto groupId = recentlyDeletedBookmark->GetGroupId(); + if (!HasBmCategory(groupId)) + groupId = LastEditedBMCategory(); + + auto bookmark = CreateBookmark(std::move(data)); + bookmark->Attach(groupId); + + auto * group = GetBmCategory(groupId); + group->AttachUserMark(bookmark->GetId()); + m_changesTracker.OnAttachBookmark(bookmark->GetId(), groupId); + group->SetIsVisible(true); + + RemoveBookmarkFromRecentlyDeleted(bookmarkId); + return bookmark; +} + void BookmarkManager::DetachUserMark(kml::MarkId bmId, kml::MarkGroupId catId) { GetGroup(catId)->DetachUserMark(bmId); @@ -456,11 +494,48 @@ void BookmarkManager::DeleteTrack(kml::TrackId trackId) auto it = m_tracks.find(trackId); auto const groupId = it->second->GetGroupId(); if (groupId != kml::kInvalidMarkGroupId) + { + AddTrackToRecentlyDeleted(std::move(it->second)); GetBmCategory(groupId)->DetachTrack(trackId); + } m_changesTracker.OnDeleteLine(trackId); m_tracks.erase(it); } +Track * BookmarkManager::RecoverRecentlyDeletedTrack(kml::TrackId trackId) +{ + CHECK_THREAD_CHECKER(m_threadChecker, ()); + auto it = m_recentlyDeletedTracks.find(trackId); + if (it == m_recentlyDeletedTracks.end()) + return nullptr; + + auto recentlyDeletedTrack = it->second.get(); + auto data = recentlyDeletedTrack->GetData(); + + auto groupId = recentlyDeletedTrack->GetGroupId(); + if (!HasBmCategory(groupId)) + groupId = LastEditedBMCategory(); + + auto track = CreateTrack(std::move(data)); + track->Attach(groupId); + + auto * group = GetBmCategory(groupId); + group->AttachTrack(trackId); + + RemoveTrackFromRecentlyDeleted(trackId); + return track; +} + +void BookmarkManager::AddTrackToRecentlyDeleted(std::unique_ptr && track) +{ + m_recentlyDeletedTracks.emplace(track.get()->GetId(), std::move(track)); +} + +void BookmarkManager::RemoveTrackFromRecentlyDeleted(kml::TrackId trackId) +{ + m_recentlyDeletedTracks.erase(trackId); +} + void BookmarkManager::GetDirtyGroups(kml::GroupIdSet & dirtyGroups) const { CHECK_THREAD_CHECKER(m_threadChecker, ()); @@ -3446,11 +3521,21 @@ Bookmark * BookmarkManager::EditSession::CreateBookmark(kml::BookmarkData && bmD return m_bmManager.CreateBookmark(std::move(bmData), groupId); } +Bookmark * BookmarkManager::EditSession::RecoverRecentlyDeletedBookmark(kml::MarkId bookmarkId) +{ + return m_bmManager.RecoverRecentlyDeletedBookmark(bookmarkId); +} + Track * BookmarkManager::EditSession::CreateTrack(kml::TrackData && trackData) { return m_bmManager.CreateTrack(std::move(trackData)); } +Track * BookmarkManager::EditSession::RecoverRecentlyDeletedTrack(kml::TrackId trackId) +{ + return m_bmManager.RecoverRecentlyDeletedTrack(trackId); +} + Bookmark * BookmarkManager::EditSession::GetBookmarkForEdit(kml::MarkId bmId) { return m_bmManager.GetBookmarkForEdit(bmId); diff --git a/map/bookmark_manager.hpp b/map/bookmark_manager.hpp index 2d673011af..d035e43504 100644 --- a/map/bookmark_manager.hpp +++ b/map/bookmark_manager.hpp @@ -165,6 +165,9 @@ public: void NotifyChanges(); + Bookmark * RecoverRecentlyDeletedBookmark(kml::MarkId bookmarkId); + Track * RecoverRecentlyDeletedTrack(kml::TrackId trackId); + private: BookmarkManager & m_bmManager; }; @@ -421,6 +424,9 @@ public: bool IsCompilation(kml::MarkGroupId id) const; kml::CompilationType GetCompilationType(kml::MarkGroupId id) const; + void RemoveBookmarkFromRecentlyDeleted(kml::MarkId bookmarkId); + void RemoveTrackFromRecentlyDeleted(kml::TrackId trackId); + private: class MarksChangesTracker : public df::UserMarksProvider { @@ -560,14 +566,19 @@ private: void DetachUserMark(kml::MarkId bmId, kml::MarkGroupId catId); void DeleteCompilations(kml::GroupIdCollection const & compilations); - Track * CreateTrack(kml::TrackData && trackData); + void AddBookmarkToRecentlyDeleted(std::unique_ptr && bookmark); + Bookmark * RecoverRecentlyDeletedBookmark(kml::MarkId bookmarkId); + Track * CreateTrack(kml::TrackData && trackData); Track * GetTrackForEdit(kml::TrackId trackId); void AttachTrack(kml::TrackId trackId, kml::MarkGroupId groupId); void DetachTrack(kml::TrackId trackId, kml::MarkGroupId groupId); void DeleteTrack(kml::TrackId trackId); void MoveTrack(kml::TrackId trackID, kml::MarkGroupId curGroupID, kml::MarkGroupId newGroupID); + void AddTrackToRecentlyDeleted(std::unique_ptr && track); + Track * RecoverRecentlyDeletedTrack(kml::TrackId trackId); + void ClearGroup(kml::MarkGroupId groupId); void SetIsVisible(kml::MarkGroupId groupId, bool visible); @@ -751,7 +762,9 @@ private: MarksCollection m_userMarks; BookmarksCollection m_bookmarks; + BookmarksCollection m_recentlyDeletedBookmarks; TracksCollection m_tracks; + TracksCollection m_recentlyDeletedTracks; StaticMarkPoint * m_selectionMark = nullptr; MyPositionMarkPoint * m_myPositionMark = nullptr; -- 2.45.3 From 58de46a6efa8df91a6cd98054d79d2d0ad3b0077 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Wed, 29 May 2024 20:28:53 +0400 Subject: [PATCH 2/5] [ios] add `onBookmarksCategoryUpdated` observing to BM manager Signed-off-by: Kiryl Kaveryn --- .../CoreApi/Bookmarks/MWMBookmarksManager.mm | 37 +++++++++++++++++-- .../CoreApi/Bookmarks/MWMBookmarksObserver.h | 1 + 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.mm b/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.mm index f9ff585c74..a4e470d921 100644 --- a/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.mm +++ b/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.mm @@ -191,6 +191,13 @@ static KmlFileType convertFileTypeToCore(MWMKmlFileType fileType) { self.bm.SetAsyncLoadingCallbacks(std::move(bookmarkCallbacks)); } +- (void)notifyObserversOnCategoryUpdated:(MWMMarkGroupID)groupId { + [self loopObservers:^(id observer) { + if ([observer respondsToSelector:@selector(onBookmarksCategoryUpdated:)]) + [observer onBookmarksCategoryUpdated:groupId]; + }]; +} + #pragma mark - Bookmarks loading - (BOOL)areBookmarksLoaded @@ -489,15 +496,17 @@ static KmlFileType convertFileTypeToCore(MWMKmlFileType fileType) { - (void)deleteBookmark:(MWMMarkID)bookmarkId { + auto const groupId = self.bm.GetBookmark(bookmarkId)->GetGroupId(); self.bm.GetEditSession().DeleteBookmark(bookmarkId); - [self loopObservers:^(id observer) { - if ([observer respondsToSelector:@selector(onBookmarkDeleted:)]) - [observer onBookmarkDeleted:bookmarkId]; - }]; + if (self.bm.HasBmCategory(groupId)) + [self notifyObserversOnCategoryUpdated:groupId]; } - (void)deleteTrack:(MWMTrackID)trackId { + auto const groupId = self.bm.GetTrack(trackId)->GetGroupId(); self.bm.GetEditSession().DeleteTrack(trackId); + if (self.bm.HasBmCategory(groupId)) + [self notifyObserversOnCategoryUpdated:groupId]; } - (MWMBookmark *)bookmarkWithId:(MWMMarkID)bookmarkId { @@ -698,6 +707,10 @@ static KmlFileType convertFileTypeToCore(MWMKmlFileType fileType) { bookmark->SetDescription(description.UTF8String); if (title.UTF8String != bookmark->GetPreferredName()) bookmark->SetCustomName(title.UTF8String); + + [self notifyObserversOnCategoryUpdated:groupId]; + if (currentGroupId != groupId) + [self notifyObserversOnCategoryUpdated:currentGroupId]; } - (void)updateBookmark:(MWMMarkID)bookmarkId setColor:(MWMBookmarkColor)color { @@ -711,6 +724,8 @@ static KmlFileType convertFileTypeToCore(MWMKmlFileType fileType) { self.bm.SetLastEditedBmColor(kmlColor); bookmark->SetColor(kmlColor); + + [self notifyObserversOnCategoryUpdated:bookmark->GetGroupId()]; } - (void)moveBookmark:(MWMMarkID)bookmarkId toGroupId:(MWMMarkGroupID)groupId { @@ -720,6 +735,10 @@ static KmlFileType convertFileTypeToCore(MWMKmlFileType fileType) { auto editSession = self.bm.GetEditSession(); editSession.MoveBookmark(bookmarkId, currentGroupId, groupId); } + + [self notifyObserversOnCategoryUpdated:groupId]; + if (currentGroupId != groupId) + [self notifyObserversOnCategoryUpdated:currentGroupId]; } - (void)updateTrack:(MWMTrackID)trackId @@ -742,6 +761,10 @@ static KmlFileType convertFileTypeToCore(MWMKmlFileType fileType) { track->SetColor(newColor); track->SetName(title.UTF8String); + + [self notifyObserversOnCategoryUpdated:groupId]; + if (currentGroupId != groupId) + [self notifyObserversOnCategoryUpdated:currentGroupId]; } - (void)updateTrack:(MWMTrackID)trackId setColor:(UIColor *)color { @@ -755,6 +778,8 @@ static KmlFileType convertFileTypeToCore(MWMKmlFileType fileType) { if (newColor != currentColor) track->SetColor(newColor); + + [self notifyObserversOnCategoryUpdated:track->GetGroupId()]; } - (void)moveTrack:(MWMTrackID)trackId toGroupId:(MWMMarkGroupID)groupId { @@ -764,6 +789,10 @@ static KmlFileType convertFileTypeToCore(MWMKmlFileType fileType) { auto editSession = self.bm.GetEditSession(); editSession.MoveTrack(trackId, currentGroupId, groupId); } + + [self notifyObserversOnCategoryUpdated:groupId]; + if (currentGroupId != groupId) + [self notifyObserversOnCategoryUpdated:currentGroupId]; } - (void)setCategory:(MWMMarkGroupID)groupId authorType:(MWMBookmarkGroupAuthorType)author diff --git a/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksObserver.h b/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksObserver.h index c24baded9f..c6f75f613b 100644 --- a/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksObserver.h +++ b/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksObserver.h @@ -9,6 +9,7 @@ NS_SWIFT_NAME(BookmarksObserver) - (void)onBookmarksLoadFinished; - (void)onBookmarksFileLoadSuccess; - (void)onBookmarksFileLoadError; +- (void)onBookmarksCategoryUpdated:(MWMMarkGroupID)groupId; - (void)onBookmarksCategoryDeleted:(MWMMarkGroupID)groupId; - (void)onBookmarkDeleted:(MWMMarkID)bookmarkId; -- 2.45.3 From 82a61269407f009349c09e5e19884f5ccbf257e4 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Thu, 23 May 2024 19:41:18 +0400 Subject: [PATCH 3/5] [ios] add Recovery support to the BM manager Signed-off-by: Kiryl Kaveryn --- .../CoreApi/Bookmarks/MWMBookmarksManager.h | 6 +++++ .../CoreApi/Bookmarks/MWMBookmarksManager.mm | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.h b/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.h index 54d395a185..b752265941 100644 --- a/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.h +++ b/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.h @@ -143,6 +143,12 @@ NS_SWIFT_NAME(BookmarksManager) - (void)moveTrack:(MWMTrackID)trackId toGroupId:(MWMMarkGroupID)groupId; +- (void)recoverBookmark:(MWMMarkID)bookmarkId; +- (void)recoverTrack:(MWMTrackID)trackId; + +- (void)removeBookmarkFromRecentlyDeleted:(MWMMarkID)bookmarkId; +- (void)removeTrackFromRecentlyDeleted:(MWMTrackID)trackId; + - (instancetype)init __attribute__((unavailable("call +manager instead"))); - (instancetype)copy __attribute__((unavailable("call +manager instead"))); - (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +manager instead"))); diff --git a/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.mm b/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.mm index a4e470d921..883bae21b6 100644 --- a/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.mm +++ b/iphone/CoreApi/CoreApi/Bookmarks/MWMBookmarksManager.mm @@ -741,6 +741,28 @@ static KmlFileType convertFileTypeToCore(MWMKmlFileType fileType) { [self notifyObserversOnCategoryUpdated:currentGroupId]; } +- (void)recoverBookmark:(MWMMarkID)bookmarkId { + Bookmark * bookmark = self.bm.GetEditSession().RecoverRecentlyDeletedBookmark(bookmarkId); + if (bookmark) + [self notifyObserversOnCategoryUpdated:bookmark->GetGroupId()]; +} + +- (void)removeBookmarkFromRecentlyDeleted:(MWMMarkID)bookmarkId +{ + self.bm.RemoveBookmarkFromRecentlyDeleted(bookmarkId); +} + +- (void)recoverTrack:(MWMTrackID)trackId { + Track * track = self.bm.GetEditSession().RecoverRecentlyDeletedTrack(trackId); + if (track) + [self notifyObserversOnCategoryUpdated:track->GetGroupId()]; +} + +- (void)removeTrackFromRecentlyDeleted:(MWMTrackID)trackId +{ + self.bm.RemoveTrackFromRecentlyDeleted(trackId); +} + - (void)updateTrack:(MWMTrackID)trackId setGroupId:(MWMMarkGroupID)groupId color:(UIColor *)color -- 2.45.3 From 91b3e09a384264cd74cb84cd2b551c56dc7fe190 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Thu, 23 May 2024 19:45:40 +0400 Subject: [PATCH 4/5] [ios] refactor the `Toast` to support Undo - Now Toast is a facade to hide the subclass implementations from the callers - New `UndoToast` class Signed-off-by: Kiryl Kaveryn --- .../Classes/CustomAlert/Toast/Toast.swift | 184 ++++++++++++++---- 1 file changed, 145 insertions(+), 39 deletions(-) diff --git a/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift b/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift index 44ebd057eb..f64783d60b 100644 --- a/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift +++ b/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift @@ -1,62 +1,53 @@ @objc(MWMToast) -final class Toast: NSObject { +class Toast: NSObject { @objc(MWMToastAlignment) enum Alignment: Int { case bottom case top } - private var blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - private var timer: Timer? - + private static let duration: TimeInterval = 3.0 private static var toasts: [Toast] = [] - @objc static func toast(withText text: String) -> Toast { - let toast = Toast(text) - toasts.append(toast) - return toast + private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + private var timer: Timer? + + var contentView: UIView { blurView.contentView } + var onHideCompletion: (() -> Void)? + + override init() { + super.init() + setupBlurView() + Self.toasts.append(self) } - - @objc static func hideAll() { - toasts.forEach { $0.hide() } - } - - private init(_ text: String) { + + private func setupBlurView() { blurView.layer.setCorner(radius: 8) blurView.clipsToBounds = true blurView.alpha = 0 - - let label = UILabel() - label.text = text - label.textAlignment = .center - label.numberOfLines = 0 - label.font = .regular14() - label.textColor = .white - label.translatesAutoresizingMaskIntoConstraints = false - blurView.contentView.addSubview(label) - blurView.isUserInteractionEnabled = false - - NSLayoutConstraint.activate([ - label.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor, constant: 8), - label.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor, constant: -8), - label.topAnchor.constraint(equalTo: blurView.contentView.topAnchor, constant: 8), - label.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor, constant: -8) - ]) } - + deinit { timer?.invalidate() } +} + +// MARK: - Public methods +extension Toast { + @objc static func hideAll() { + toasts.forEach { $0.hide() } + } @objc func show() { - show(in: UIApplication.shared.keyWindow, alignment: .bottom) + Self.hideAll() + show(in: UIApplication.shared.keyWindow, alignment: .bottom, duration: Toast.duration) } - @objc func show(withAlignment alignment: Alignment, pinToSafeArea: Bool = true) { - show(in: UIApplication.shared.keyWindow, alignment: alignment, pinToSafeArea: pinToSafeArea) + @objc func show(withAlignment alignment: Alignment, pinToSafeArea: Bool = true, duration: TimeInterval = Toast.duration) { + show(in: UIApplication.shared.keyWindow, alignment: alignment, pinToSafeArea: pinToSafeArea, duration: duration) } - @objc func show(in view: UIView?, alignment: Alignment, pinToSafeArea: Bool = true) { + @objc func show(in view: UIView?, alignment: Alignment, pinToSafeArea: Bool = true, duration: TimeInterval = Toast.duration) { guard let view = view else { return } blurView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(blurView) @@ -85,7 +76,7 @@ final class Toast: NSObject { self.blurView.alpha = 1 } - timer = Timer.scheduledTimer(timeInterval: 3, + timer = Timer.scheduledTimer(timeInterval: duration, target: self, selector: #selector(hide), userInfo: nil, @@ -96,9 +87,124 @@ final class Toast: NSObject { timer?.invalidate() if self.blurView.superview != nil { UIView.animate(withDuration: kDefaultAnimationDuration, - animations: { self.blurView.alpha = 0 }) { [self] _ in + animations: { self.blurView.alpha = 0 }, + completion: { [self] _ in + self.onHideCompletion?() self.blurView.removeFromSuperview() - Self.toasts.removeAll(where: { $0 === self }) } + Self.toasts.removeAll(where: { $0 === self }) + }) } } } + +extension Toast { + @objc static func toast(withText text: String) -> Toast { + toast(withText: text, onHideCompletion: nil) + } + + @objc static func toast(withText text: String, onHideCompletion: (() -> Void)? = nil) -> Toast { + DefaultToast(text, onHideCompletion: onHideCompletion) + } + + @objc static func undoToast(withText text: String, undoAction: @escaping () -> Void, onHideCompletion: (() -> Void)?) -> Toast { + UndoToast(text, undoAction: undoAction, onHideCompletion: onHideCompletion) + } + + @objc static func undoToast(deletedObject: String, undoAction: @escaping () -> Void, onHideCompletion: (() -> Void)? = nil) -> Toast { + // TODO: localize text + undoToast(withText: "The \(deletedObject) was deleted", undoAction: undoAction, onHideCompletion: onHideCompletion) + } +} + +private final class DefaultToast: Toast { + + private let messageLabel = UILabel() + + fileprivate convenience init(_ text: String, onHideCompletion: (() -> Void)?) { + self.init() + self.onHideCompletion = onHideCompletion + setupMessageLabel(text) + layoutViews() + } + + private func setupMessageLabel(_ text: String) { + messageLabel.text = text + messageLabel.textAlignment = .center + messageLabel.numberOfLines = 0 + messageLabel.font = .regular14() + messageLabel.textColor = .white + messageLabel.isUserInteractionEnabled = false + } + + private func layoutViews() { + contentView.addSubview(messageLabel) + + messageLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + messageLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), + messageLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8), + messageLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + messageLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) + ]) + } +} + +private final class UndoToast: Toast { + + private let messageLabel = UILabel() + private let undoButton = UIButton() + private var undoAction: (() -> Void)? + + fileprivate convenience init(_ text: String, undoAction: @escaping () -> Void, onHideCompletion: (() -> Void)?) { + self.init() + self.undoAction = undoAction + self.onHideCompletion = onHideCompletion + setupMessageLabel(text) + setupUndoButton() + layoutViews() + } + + private func setupMessageLabel(_ text: String) { + messageLabel.text = text + messageLabel.textAlignment = .center + messageLabel.numberOfLines = 0 + messageLabel.font = .regular14() + messageLabel.textColor = .white + messageLabel.isUserInteractionEnabled = false + } + + private func setupUndoButton() { + undoButton.setTitle(L("undo"), for: .normal) + undoButton.setTitleColor(.white, for: .normal) + undoButton.setTitleColor(.lightGray, for: .highlighted) + undoButton.titleLabel?.font = .bold17() + undoButton.backgroundColor = .clear + undoButton.addTarget(self, action: #selector(undoButtonDidTap), for: .touchUpInside) + } + + @objc private func undoButtonDidTap() { + undoAction?() + hide() + } + + private func layoutViews() { + contentView.addSubview(messageLabel) + contentView.addSubview(undoButton) + + messageLabel.translatesAutoresizingMaskIntoConstraints = false + undoButton.translatesAutoresizingMaskIntoConstraints = false + messageLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + NSLayoutConstraint.activate([ + contentView.heightAnchor.constraint(greaterThanOrEqualToConstant: 40), + + messageLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12), + messageLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + messageLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8), + + undoButton.leadingAnchor.constraint(equalTo: messageLabel.trailingAnchor, constant: 20), + undoButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -25), + undoButton.centerYAnchor.constraint(equalTo: messageLabel.centerYAnchor), + undoButton.heightAnchor.constraint(equalToConstant: 30), + ]) + } +} -- 2.45.3 From 628de3b06235bdb7f898c7722f2fd7cf1449c2e9 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Thu, 23 May 2024 19:53:42 +0400 Subject: [PATCH 5/5] [ios] implement Recovery for the Recently Deleted bookmarks Signed-off-by: Kiryl Kaveryn --- .../BookmarksListInteractor.swift | 27 +++++++++++++++++-- .../BookmarksListInterfaces.swift | 8 ++++-- .../BookmarksListPresenter.swift | 22 +++++++++++++-- .../BMCView/BMCViewController.swift | 4 +++ .../BMCViewModel/BMCDefaultViewModel.swift | 5 +++- .../EditBookmarkViewController.swift | 9 ++++++- .../EditTrackViewController.swift | 10 ++++++- .../Components/ActionBarViewController.swift | 7 +++++ .../ActionBar/MWMActionBarButton.h | 2 ++ .../ActionBar/MWMActionBarButton.m | 7 ++--- .../Layouts/PlacePageCommonLayout.swift | 2 ++ .../PlacePageManager/MWMPlacePageManager.mm | 27 ++++++++++++++++--- 12 files changed, 115 insertions(+), 15 deletions(-) diff --git a/iphone/Maps/Bookmarks/BookmarksList/BookmarksListInteractor.swift b/iphone/Maps/Bookmarks/BookmarksList/BookmarksListInteractor.swift index af144e5151..d8c86738b3 100644 --- a/iphone/Maps/Bookmarks/BookmarksList/BookmarksListInteractor.swift +++ b/iphone/Maps/Bookmarks/BookmarksList/BookmarksListInteractor.swift @@ -121,14 +121,32 @@ extension BookmarksListInteractor: IBookmarksListInteractor { return BookmarksListSortingType(bookmarksManager.lastSortingType(markGroupId)) } - func deleteBookmark(_ bookmarkId: MWMMarkID) { + func deleteBookmark(_ bookmarkId: MWMMarkID, completion: (Bookmark) -> Void) { + completion(bookmarksManager.bookmark(withId: bookmarkId)) bookmarksManager.deleteBookmark(bookmarkId) } - func deleteTrack(_ trackId: MWMTrackID) { + func deleteTrack(_ trackId: MWMTrackID, completion: (Track) -> Void) { + completion(bookmarksManager.track(withId: trackId)) bookmarksManager.deleteTrack(trackId) } + func recoverBookmark(_ bookmarkId: MWMMarkID) { + bookmarksManager.recoverBookmark(bookmarkId) + } + + func recoverTrack(_ trackId: MWMTrackID) { + bookmarksManager.recoverTrack(trackId) + } + + func removeBookmarkFromRecentlyDeleted(_ bookmarkId: MWMMarkID) { + bookmarksManager.removeBookmark(fromRecentlyDeleted: bookmarkId) + } + + func removeTrackFromRecentlyDeleted(_ trackId: MWMTrackID) { + bookmarksManager.removeTrack(fromRecentlyDeleted: trackId) + } + func moveBookmark(_ bookmarkId: MWMMarkID, toGroupId groupId: MWMMarkGroupID) { bookmarksManager.moveBookmark(bookmarkId, toGroupId: groupId) } @@ -183,4 +201,9 @@ extension BookmarksListInteractor: BookmarksObserver { func onBookmarksCategoryDeleted(_ groupId: MWMMarkGroupID) { reloadCategory() } + + func onBookmarksCategoryUpdated(_ groupId: MWMMarkGroupID) { + guard groupId == markGroupId else { return } + reloadCategory() + } } diff --git a/iphone/Maps/Bookmarks/BookmarksList/BookmarksListInterfaces.swift b/iphone/Maps/Bookmarks/BookmarksList/BookmarksListInterfaces.swift index 6767d392d3..fd0bed6249 100644 --- a/iphone/Maps/Bookmarks/BookmarksList/BookmarksListInterfaces.swift +++ b/iphone/Maps/Bookmarks/BookmarksList/BookmarksListInterfaces.swift @@ -110,8 +110,12 @@ protocol IBookmarksListInteractor { completion: @escaping ([BookmarksSection]) -> Void) func resetSort() func lastSortingType() -> BookmarksListSortingType? - func deleteBookmark(_ bookmarkId: MWMMarkID) - func deleteTrack(_ trackId: MWMTrackID) + func deleteBookmark(_ bookmarkId: MWMMarkID, completion: (Bookmark) -> Void) + func deleteTrack(_ trackId: MWMTrackID, completion: (Track) -> Void) + func recoverBookmark(_ bookmarkId: MWMMarkID) + func recoverTrack(_ trackId: MWMTrackID) + func removeBookmarkFromRecentlyDeleted(_ bookmarkId: MWMMarkID) + func removeTrackFromRecentlyDeleted(_ trackId: MWMTrackID) func moveBookmark(_ bookmarkId: MWMMarkID, toGroupId: MWMMarkGroupID) func moveTrack(_ trackId: MWMTrackID, toGroupId: MWMMarkGroupID) func updateBookmark(_ bookmarkId: MWMMarkID, setGroupId groupId: MWMMarkGroupID, title: String, color: BookmarkColor, description: String) diff --git a/iphone/Maps/Bookmarks/BookmarksList/BookmarksListPresenter.swift b/iphone/Maps/Bookmarks/BookmarksList/BookmarksListPresenter.swift index b944c80db5..8a74d8f2e7 100644 --- a/iphone/Maps/Bookmarks/BookmarksList/BookmarksListPresenter.swift +++ b/iphone/Maps/Bookmarks/BookmarksList/BookmarksListPresenter.swift @@ -273,11 +273,29 @@ extension BookmarksListPresenter: IBookmarksListPresenter { switch section { case let bookmarksSection as IBookmarksSectionViewModel: guard let bookmark = bookmarksSection.bookmarks[index] as? BookmarkViewModel else { fatalError() } - interactor.deleteBookmark(bookmark.bookmarkId) + interactor.deleteBookmark(bookmark.bookmarkId) { bookmark in + // TODO: localize texts + Toast.undoToast(deletedObject: bookmark.bookmarkName, + undoAction: { [weak self, interactor] in + interactor.recoverBookmark(bookmark.bookmarkId) + self?.reload() + }, onHideCompletion: { [interactor] in + interactor.removeBookmarkFromRecentlyDeleted(bookmark.bookmarkId) + }).show() + } reload() case let tracksSection as ITracksSectionViewModel: guard let track = tracksSection.tracks[index] as? TrackViewModel else { fatalError() } - interactor.deleteTrack(track.trackId) + interactor.deleteTrack(track.trackId) { track in + // TODO: localize texts + Toast.undoToast(deletedObject: track.trackName, + undoAction: { [weak self, interactor] in + interactor.recoverTrack(track.trackId) + self?.reload() + }, onHideCompletion: { [interactor] in + interactor.removeTrackFromRecentlyDeleted(track.trackId) + }).show() + } reload() default: fatalError("Cannot delete item: unsupported section type: \(section.self)") diff --git a/iphone/Maps/Bookmarks/Categories/BMCView/BMCViewController.swift b/iphone/Maps/Bookmarks/Categories/BMCView/BMCViewController.swift index 51fbd45775..91f7698579 100644 --- a/iphone/Maps/Bookmarks/Categories/BMCView/BMCViewController.swift +++ b/iphone/Maps/Bookmarks/Categories/BMCView/BMCViewController.swift @@ -185,6 +185,10 @@ extension BMCViewController: BMCView { } } } + + func isVisible() -> Bool { + isViewLoaded && view.window != nil + } } extension BMCViewController: UITableViewDataSource { diff --git a/iphone/Maps/Bookmarks/Categories/BMCViewModel/BMCDefaultViewModel.swift b/iphone/Maps/Bookmarks/Categories/BMCViewModel/BMCDefaultViewModel.swift index 69195c361b..d742d60db2 100644 --- a/iphone/Maps/Bookmarks/Categories/BMCViewModel/BMCDefaultViewModel.swift +++ b/iphone/Maps/Bookmarks/Categories/BMCViewModel/BMCDefaultViewModel.swift @@ -3,6 +3,7 @@ protocol BMCView: AnyObject { func delete(at indexPaths: [IndexPath]) func insert(at indexPaths: [IndexPath]) func conversionFinished(success: Bool) + func isVisible() -> Bool } final class BMCDefaultViewModel: NSObject { @@ -169,10 +170,12 @@ extension BMCDefaultViewModel: BookmarksObserver { } func onBookmarksCategoryDeleted(_ groupId: MWMMarkGroupID) { + guard let view, view.isVisible() else { return } reloadData() } - func onBookmarkDeleted(_: MWMMarkID) { + func onBookmarksCategoryUpdated(_ groupId: MWMMarkGroupID) { + guard let view, view.isVisible() else { return } reloadData() } } diff --git a/iphone/Maps/UI/EditBookmark/EditBookmarkViewController.swift b/iphone/Maps/UI/EditBookmark/EditBookmarkViewController.swift index 2d0911df1b..947a08c703 100644 --- a/iphone/Maps/UI/EditBookmark/EditBookmarkViewController.swift +++ b/iphone/Maps/UI/EditBookmark/EditBookmarkViewController.swift @@ -234,7 +234,14 @@ extension EditBookmarkViewController: MWMNoteCellDelegate { extension EditBookmarkViewController: MWMButtonCellDelegate { func cellDidPressButton(_ cell: UITableViewCell) { - BookmarksManager.shared().deleteBookmark(bookmarkId) + let bookmarkManager = BookmarksManager.shared() + bookmarkManager.deleteBookmark(bookmarkId) + Toast.undoToast(deletedObject: bookmarkTitle ?? L("bookmark"), + undoAction: { [bookmarkId] in + bookmarkManager.recoverBookmark(bookmarkId) + }, onHideCompletion: { [bookmarkId] in + bookmarkManager.removeBookmark(fromRecentlyDeleted: bookmarkId) + }).show() if let placePageData = placePageData { FrameworkHelper.updateAfterDeleteBookmark() placePageData.updateBookmarkStatus() diff --git a/iphone/Maps/UI/EditBookmark/EditTrackViewController.swift b/iphone/Maps/UI/EditBookmark/EditTrackViewController.swift index f82e3d956a..7988b2bfb5 100644 --- a/iphone/Maps/UI/EditBookmark/EditTrackViewController.swift +++ b/iphone/Maps/UI/EditBookmark/EditTrackViewController.swift @@ -186,7 +186,15 @@ extension EditTrackViewController: BookmarkTitleCellDelegate { extension EditTrackViewController: MWMButtonCellDelegate { func cellDidPressButton(_ cell: UITableViewCell) { - bookmarksManager.deleteTrack(trackId) + let bookmarkManager = BookmarksManager.shared() + bookmarkManager.deleteTrack(trackId) + Toast.undoToast(withText: trackTitle ?? L("track_title"), + undoAction: { [trackId] in + bookmarkManager.recoverTrack(trackId) + }, onHideCompletion: { [trackId] in + bookmarkManager.removeTrack(fromRecentlyDeleted: trackId) + }).show() + // TODO: When the PlacePage screen will be implemented it should be reloaded here (see EditBookmarkViewController). goBack() } } diff --git a/iphone/Maps/UI/PlacePage/Components/ActionBarViewController.swift b/iphone/Maps/UI/PlacePage/Components/ActionBarViewController.swift index 35c8843e3f..6a93474886 100644 --- a/iphone/Maps/UI/PlacePage/Components/ActionBarViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/ActionBarViewController.swift @@ -64,6 +64,13 @@ class ActionBarViewController: UIViewController { configureButtons() } + func setBookmarkSelected(_ isSelected: Bool) { + guard let bookmarkButton = stackView.arrangedSubviews.first(where: { ($0 as? ActionBarButton)?.type == .bookmark }) as? ActionBarButton else { + return + } + bookmarkButton.setBookmarkSelected(isSelected) + } + override func viewDidLoad() { super.viewDidLoad() diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h index 52ea9baa63..e8aab9bf6c 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.h @@ -47,6 +47,8 @@ NS_SWIFT_NAME(ActionBarButton) isSelected:(BOOL)isSelected isEnabled:(BOOL)isEnabled; +- (void)setBookmarkSelected:(BOOL)isSelected; + @end NS_ASSUME_NONNULL_END diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m index 1b4da13d6a..4cb3e07e5c 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/ActionBar/MWMActionBarButton.m @@ -155,8 +155,6 @@ NSString *titleForButton(MWMActionBarButtonType type, BOOL isSelected) { } - (IBAction)tap { - if (self.type == MWMActionBarButtonTypeBookmark) - [self setBookmarkSelected:!self.button.isSelected]; if (self.type == MWMActionBarButtonTypeRouteTo) [self disableRouteToButtonHighlight]; @@ -164,7 +162,10 @@ NSString *titleForButton(MWMActionBarButtonType type, BOOL isSelected) { } - (void)setBookmarkSelected:(BOOL)isSelected { - if (isSelected) + if (self.type != MWMActionBarButtonTypeBookmark) + return; + + if (!self.button.isSelected && isSelected) [self.button.imageView startAnimating]; self.button.selected = isSelected; diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift index 2e9ec14f88..a24ff0817b 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift @@ -187,6 +187,7 @@ extension PlacePageCommonLayout { bookmarkViewController.bookmarkData = bookmarkData isBookmark = true } + actionBarViewController.setBookmarkSelected(isBookmark) if let title = placePageData.previewData.title, let headerViewController = headerViewControllers.compactMap({ $0 as? PlacePageHeaderViewController }).first { let secondaryTitle = placePageData.previewData.secondaryTitle headerViewController.setTitle(title, secondaryTitle: secondaryTitle) @@ -195,6 +196,7 @@ extension PlacePageCommonLayout { self.presenter?.layoutIfNeeded() UIView.animate(withDuration: kDefaultAnimationDuration) { [unowned self] in self.bookmarkViewController.view.isHidden = !isBookmark + self.actionBar?.placePageData = self.placePageData self.presenter?.layoutIfNeeded() } } diff --git a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm index 779c5e90d4..2db7613ebd 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm +++ b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm @@ -187,11 +187,32 @@ using namespace storage; - (void)removeBookmark:(PlacePageData *)data { - auto &f = GetFramework(); - f.GetBookmarkManager().GetEditSession().DeleteBookmark(data.bookmarkData.bookmarkId); + PlacePageBookmarkData * bookmarkData = data.bookmarkData; + NSString * bookmarkTitle = data.previewData.title; + + [MWMBookmarksManager.sharedManager deleteBookmark:bookmarkData.bookmarkId]; + + [[MWMToast + undoToastWithDeletedObject:bookmarkTitle + undoAction:^{ + [MWMBookmarksManager.sharedManager recoverBookmark:bookmarkData.bookmarkId]; + // Skip updating PP if it's closed. + if (![self isPPShown]) + return; + + auto & f = GetFramework(); + auto & info = f.GetCurrentPlacePageInfo(); + auto buildInfo = info.GetBuildInfo(); + buildInfo.m_match = place_page::BuildInfo::Match::Everything; + buildInfo.m_userMarkId = bookmarkData.bookmarkId; + f.UpdatePlacePageInfoForCurrentSelection(buildInfo); + [data updateBookmarkStatus]; + } + onHideCompletion:^{ + [MWMBookmarksManager.sharedManager removeBookmarkFromRecentlyDeleted:bookmarkData.bookmarkId]; + }] show]; [MWMFrameworkHelper updateAfterDeleteBookmark]; - [data updateBookmarkStatus]; } -- 2.45.3