From 87d9e5df2a913680813feaa6780fcdffb5b7fe88 Mon Sep 17 00:00:00 2001 From: Harry <109690906+harry-signal@users.noreply.github.com> Date: Tue, 6 Sep 2022 09:48:06 -0700 Subject: [PATCH] Set story hidden state from stories view controller context menu * Set hidden state from stories view controller context menu * fix toasts when shown on controllers with UIScrollViews as their root views * delete when hiding the onboarding story * run translations script * respond to pr feedback * add hidden state to systemStoryManager * use hidden state from SystemStoryManager for onboarding story * remove unused method --- Signal.xcodeproj/project.pbxproj | 4 + .../StoryHidingManager.swift | 190 ++++++++++++++++++ .../Stories/StoriesViewController.swift | 17 +- .../HomeView/Stories/StoryViewModel.swift | 12 ++ .../translations/en.lproj/Localizable.strings | 15 ++ .../Messages/Stories/SystemStoryManager.swift | 99 +++++++-- .../Stories/SystemStoryManagerMock.swift | 16 ++ .../Stories/SystemStoryManagerProtocol.swift | 16 ++ SignalUI/Views/Toast.swift | 9 +- 9 files changed, 351 insertions(+), 27 deletions(-) create mode 100644 Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryHidingManager.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 58a0e8d840..a3b2a065b9 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -773,6 +773,7 @@ 661396AF28BE881E00E0C4DF /* ChainedPromiseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661396AE28BE881E00E0C4DF /* ChainedPromiseTest.swift */; }; 668CAB3E289983520085A2C3 /* AudioMessagePlaybackRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668CAB3D289983520085A2C3 /* AudioMessagePlaybackRateView.swift */; }; 668FE09B28B923A4008B9071 /* Bool+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668FE09A28B923A4008B9071 /* Bool+SSK.swift */; }; + 668FE09F28B947ED008B9071 /* StoryHidingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668FE09E28B947ED008B9071 /* StoryHidingManager.swift */; }; 669E8FE828B4153C00043D28 /* OWSURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669E8FE728B4153B00043D28 /* OWSURLSession.swift */; }; 669E8FE928B415C000043D28 /* OWSURLBuilderUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669E8FDB28B02CC400043D28 /* OWSURLBuilderUtil.swift */; }; 669E8FED28B4177900043D28 /* OWSSignalServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669E8FEC28B4177800043D28 /* OWSSignalServiceMock.swift */; }; @@ -3026,6 +3027,7 @@ 668AB0CB28AD610600B31984 /* StoryAuthorUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryAuthorUtil.swift; sourceTree = ""; }; 668CAB3D289983520085A2C3 /* AudioMessagePlaybackRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessagePlaybackRateView.swift; sourceTree = ""; }; 668FE09A28B923A4008B9071 /* Bool+SSK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bool+SSK.swift"; sourceTree = ""; }; + 668FE09E28B947ED008B9071 /* StoryHidingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryHidingManager.swift; sourceTree = ""; }; 669E8FDB28B02CC400043D28 /* OWSURLBuilderUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSURLBuilderUtil.swift; sourceTree = ""; }; 669E8FE528B4149200043D28 /* OWSURLSessionMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSURLSessionMock.swift; sourceTree = ""; }; 669E8FE728B4153B00043D28 /* OWSURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSURLSession.swift; sourceTree = ""; }; @@ -6051,6 +6053,7 @@ children = ( 8864072F27F21AA7009916B6 /* Group Reply Sheet */, 88423A51280A171E007D2918 /* StoryDirectReplySheet.swift */, + 668FE09E28B947ED008B9071 /* StoryHidingManager.swift */, 88B00D4E28A33B5800BC9CA0 /* StoryPrivateViewsSheet.swift */, 8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */, 88423A53280A2675007D2918 /* StoryReplyPreviewView.swift */, @@ -10367,6 +10370,7 @@ 8864072827EEA658009916B6 /* StoryGroupReplySheet.swift in Sources */, 88B00D4B28A32DB600BC9CA0 /* StoryGroupReplyViewController.swift in Sources */, 8864072C27F0DA38009916B6 /* StoryGroupReplyViewItem.swift in Sources */, + 668FE09F28B947ED008B9071 /* StoryHidingManager.swift in Sources */, 88863A52280CAE6A00977F69 /* StoryInteractiveTransitionCoordinator.swift in Sources */, 884DB95227DE67D900C6A309 /* StoryItemMediaView.swift in Sources */, 884DB94F27DE67BB00C6A309 /* StoryPageViewController.swift in Sources */, diff --git a/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryHidingManager.swift b/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryHidingManager.swift new file mode 100644 index 0000000000..f317ceb60f --- /dev/null +++ b/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/StoryHidingManager.swift @@ -0,0 +1,190 @@ +// +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. +// + +import Foundation +import UIKit + +class StoryHidingManager: Dependencies { + + private let model: StoryViewModel + + init(model: StoryViewModel) { + self.model = model + } + + public func contextMenuAction( + forPresentingController presentingController: UIViewController + ) -> ContextMenuAction { + let isHidden = model.isHidden + let title: String + let image: UIImage + if isHidden { + title = NSLocalizedString( + "STORIES_UNHIDE_STORY_ACTION", + comment: "Context menu action to unhide the selected story" + ) + image = Theme.iconImage(.checkCircle) + } else { + title = NSLocalizedString( + "STORIES_HIDE_STORY_ACTION", + comment: "Context menu action to hide the selected story" + ) + image = Theme.iconImage(.xCircle24) + } + return .init( + title: title, + image: image, + handler: { [self, weak presentingController] _ in + guard let presentingController = presentingController else { + return + } + if isHidden { + self.setHideStateAndShowToast(shouldHide: !isHidden, presentingController: presentingController) + } else { + self.showActionSheetIfNeeded(shouldHide: !isHidden, on: presentingController) + } + } + ) + } + + private func showActionSheetIfNeeded(shouldHide: Bool, on presentingController: UIViewController) { + guard shouldHide else { + // No need to show anything if unhiding, hide right away + setHideStateAndShowToast(shouldHide: shouldHide, presentingController: presentingController) + return + } + + let actionSheet = createHidingActionSheetWithSneakyTransaction() + + let actionTitle: String + if shouldHide { + actionTitle = NSLocalizedString( + "STORIES_HIDE_STORY_ACTION", + comment: "Context menu action to hide the selected story" + ) + } else { + actionTitle = NSLocalizedString( + "STORIES_UNHIDE_STORY_ACTION", + comment: "Context menu action to unhide the selected story" + ) + } + + actionSheet.addAction(ActionSheetAction( + title: actionTitle, + style: .default, + handler: { [self, weak presentingController] _ in + guard let presentingController = presentingController else { + return + } + self.setHideStateAndShowToast(shouldHide: shouldHide, presentingController: presentingController) + } + )) + actionSheet.addAction(OWSActionSheets.cancelAction) + presentingController.presentActionSheet(actionSheet) + } + + // MARK: - Loading data + + private func loadThread(_ transaction: SDSAnyReadTransaction) -> TSThread? { + switch model.context { + case .groupId(let groupId): + return TSGroupThread.fetch(groupId: groupId, transaction: transaction) + case .authorUuid(let authorUuid): + return TSContactThread.getWithContactAddress( + authorUuid.asSignalServiceAddress(), + transaction: transaction + ) + case .privateStory: + owsFailDebug("Unexpectedly had private story when hiding") + return nil + case .none: + owsFailDebug("Unexpectedly missing context for story when hiding") + return nil + } + } + + // MARK: - Header configuration + + private func createHidingActionSheetWithSneakyTransaction() -> ActionSheetController { + return ActionSheetController( + title: NSLocalizedString( + "STORIES_HIDE_STORY_ACTION_SHEET_TITLE", + comment: "Title asking the user if they are sure they want to hide stories from another user" + ), + message: loadThreadDisplayNameWithSneakyTransaction().map { + String( + format: NSLocalizedString( + "STORIES_HIDE_STORY_ACTION_SHEET_MESSAGE", + comment: "Message asking the user if they are sure they want to hide stories from {{other user's name}}" + ), + $0 + ) + } + ) + } + + private func loadThreadDisplayNameWithSneakyTransaction() -> String? { + return Self.databaseStorage.read { transaction -> String? in + switch self.model.context { + case .groupId(let groupId): + return TSGroupThread.fetch(groupId: groupId, transaction: transaction)?.groupNameOrDefault + case .authorUuid(let authorUuid): + if authorUuid.asSignalServiceAddress().isSystemStoryAddress { + return NSLocalizedString( + "SYSTEM_ADDRESS_NAME", + comment: "Name to display for the 'system' sender, e.g. for release notes and the onboarding story" + ) + } + return Self.contactsManager.shortDisplayName( + for: authorUuid.asSignalServiceAddress(), + transaction: transaction + ) + case .privateStory: + owsFailDebug("Unexpectedly had private story when hiding") + return nil + case .none: + owsFailDebug("Unexpectedly missing context for story when hiding") + return nil + } + } + } + + // MARK: - Issuing changes + + private func setHideStateAndShowToast( + shouldHide: Bool, + presentingController: UIViewController + ) { + Self.databaseStorage.write { transaction in + guard self.model.messages.first?.authorAddress.isSystemStoryAddress != true else { + // System stories go through SystemStoryManager + Self.systemStoryManager.setSystemStoriesHidden(shouldHide, transaction: transaction) + return + } + guard let thread = self.model.context.thread(transaction: transaction) else { + owsFailDebug("Hiding a story without a thread") + return + } + let threadAssociatedData = ThreadAssociatedData.fetchOrDefault(for: thread, transaction: transaction) + threadAssociatedData.updateWith( + hideStory: shouldHide, + updateStorageService: true, + transaction: transaction + ) + } + let toastText: String + if shouldHide { + toastText = NSLocalizedString( + "STORIES_HIDE_STORY_CONFIRMATION_TOAST", + comment: "Toast shown when a story is successfuly hidden" + ) + } else { + toastText = NSLocalizedString( + "STORIES_UNHIDE_STORY_CONFIRMATION_TOAST", + comment: "Toast shown when a story is successfuly unhidden" + ) + } + presentingController.presentToast(text: toastText) + } +} diff --git a/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift b/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift index ec183bd9ae..63a6db5e4f 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/StoriesViewController.swift @@ -507,21 +507,18 @@ extension StoriesViewController: StoryPageViewControllerDataSource { extension StoriesViewController: ContextMenuInteractionDelegate { func contextMenuInteraction(_ interaction: ContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> ContextMenuConfiguration? { - guard let indexPath = tableView.indexPathForRow(at: location), - let model = model(for: indexPath) else { return nil } + guard + let indexPath = tableView.indexPathForRow(at: location), + let model = model(for: indexPath) + else { + return nil + } return .init(identifier: indexPath as NSCopying) { _ in var actions = [ContextMenuAction]() - actions.append(.init( - title: NSLocalizedString( - "STORIES_HIDE_STORY_ACTION", - comment: "Context menu action to hide the selected story"), - image: Theme.iconImage(.xCircle24), - handler: { _ in - OWSActionSheets.showActionSheet(title: LocalizationNotNeeded("Hiding stories is not yet implemented.")) - })) + actions.append(StoryHidingManager(model: model).contextMenuAction(forPresentingController: self)) func appendForwardAction() { actions.append(.init( diff --git a/Signal/src/ViewControllers/HomeView/Stories/StoryViewModel.swift b/Signal/src/ViewControllers/HomeView/Stories/StoryViewModel.swift index 267a95f188..29f5d5b9a0 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/StoryViewModel.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/StoryViewModel.swift @@ -11,6 +11,10 @@ struct StoryViewModel: Dependencies { let messages: [StoryMessage] let hasUnviewedMessages: Bool + // NOTE: "hidden" stories are still shown, + // just in a separate section thats collapsed by default. + let isHidden: Bool + let latestMessageAttachment: StoryThumbnailView.Attachment let hasReplies: Bool let latestMessageName: String @@ -44,6 +48,14 @@ struct StoryViewModel: Dependencies { latestMessageAttachment = .from(latestMessage.attachment, transaction: transaction) latestMessageTimestamp = latestMessage.timestamp latestMessageViewedTimestamp = latestMessage.localUserViewedTimestamp + + if latestMessage.authorAddress.isSystemStoryAddress { + self.isHidden = Self.systemStoryManager.areSystemStoriesHidden(transaction: transaction) + } else { + self.isHidden = context.thread(transaction: transaction).map { + return ThreadAssociatedData.fetchOrDefault(for: $0, transaction: transaction).hideStory + } ?? false + } } func copy(updatedMessages: [StoryMessage], deletedMessageRowIds: [Int64], transaction: SDSAnyReadTransaction) throws -> Self? { diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 7eb7140282..3776506dc3 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -6160,6 +6160,15 @@ /* Context menu action to hide the selected story */ "STORIES_HIDE_STORY_ACTION" = "Hide Story"; +/* Message asking the user if they are sure they want to hide stories from {{other user's name}} */ +"STORIES_HIDE_STORY_ACTION_SHEET_MESSAGE" = "New story updates from %@ won't appear at the top of the stories list anymore."; + +/* Title asking the user if they are sure they want to hide stories from another user */ +"STORIES_HIDE_STORY_ACTION_SHEET_TITLE" = "Hide Story?"; + +/* Toast shown when a story is successfuly hidden */ +"STORIES_HIDE_STORY_CONFIRMATION_TOAST" = "Story Hidden"; + /* Indicates that there are no recent stories to render */ "STORIES_NO_RECENT_MESSAGES" = "No updates to show right now.\nTap + to add to your story."; @@ -6205,6 +6214,12 @@ /* Title for the stories view. */ "STORIES_TITLE" = "Stories"; +/* Context menu action to unhide the selected story */ +"STORIES_UNHIDE_STORY_ACTION" = "Unhide Story"; + +/* Toast shown when a story is successfuly unhidden */ +"STORIES_UNHIDE_STORY_CONFIRMATION_TOAST" = "Story Unhidden"; + /* Title text for the 'views' tab on the stories views & replies sheet */ "STORIES_VIEWS_TAB" = "Views"; diff --git a/SignalServiceKit/src/Messages/Stories/SystemStoryManager.swift b/SignalServiceKit/src/Messages/Stories/SystemStoryManager.swift index b3b77ab819..e7592ee57a 100644 --- a/SignalServiceKit/src/Messages/Stories/SystemStoryManager.swift +++ b/SignalServiceKit/src/Messages/Stories/SystemStoryManager.swift @@ -85,6 +85,53 @@ public class SystemStoryManager: NSObject, Dependencies, SystemStoryManagerProto }.map(on: queue) { _ in () } } + // MARK: Hidden State + + private var stateChangeObservers = [SystemStoryStateChangeObserver]() + + public func addStateChangedObserver(_ observer: SystemStoryStateChangeObserver) { + stateChangeObservers.append(observer) + } + + public func removeStateChangedObserver(_ observer: SystemStoryStateChangeObserver) { + stateChangeObservers.removeAll(where: { $0 == observer }) + } + + public func areSystemStoriesHidden(transaction: SDSAnyReadTransaction) -> Bool { + // No need to make this serial with the other calls, db transactions cover us. + kvStore.getBool(Constants.kvStoreHiddenStateKey, defaultValue: false, transaction: transaction) + } + + public func setSystemStoriesHidden(_ hidden: Bool, transaction: SDSAnyWriteTransaction) { + var changedRowIds = [Int64]() + defer { + DispatchQueue.main.async { + self.stateChangeObservers.forEach { $0.systemStoryHiddenStateDidChange(rowIds: changedRowIds) } + } + } + + // No need to make this serial with the other calls, db transactions cover us. + kvStore.setBool(hidden, key: Constants.kvStoreHiddenStateKey, transaction: transaction) + + guard + let rawStatus = kvStore.getData(Constants.kvStoreOnboardingStoryStatusKey, transaction: transaction), + let onboardingStatus = try? JSONDecoder().decode(DownloadStatus.self, from: rawStatus), + let messageUniqueIds = onboardingStatus.messageUniqueIds, + !messageUniqueIds.isEmpty + else { + return + } + let stories = StoryFinder.listStoriesWithUniqueIds(messageUniqueIds, transaction: transaction) + stories.forEach { + if hidden { + $0.markAsViewed(at: Date().ows_millisecondsSince1970, circumstance: .onThisDevice, transaction: transaction) + } + if let rowId = $0.id { + changedRowIds.append(rowId) + } + } + } + // MARK: - Event Observation private var isObservingBackgrounding = false @@ -159,26 +206,38 @@ public class SystemStoryManager: NSObject, Dependencies, SystemStoryManagerProto } private func checkDownloadStatus() -> Promise { - return databaseStorage.write(.promise) { [kvStore] transaction in - guard - let rawStatus = kvStore.getData(Constants.kvStoreStatusKey, transaction: transaction), - let status = try? JSONDecoder().decode(DownloadStatus.self, from: rawStatus) - else { - return .requiresDownload + return databaseStorage.write(.promise) { [weak self] transaction in + guard let strongSelf = self else { + throw OWSAssertionError("SystemStoryManager unretained") } - if status.isDownloaded { - // clean up opportunistically. - try? self.cleanUpStoriesIfNeeded( - messageUniqueIds: status.messageUniqueIds, - transaction: transaction - ) - } - return status + return strongSelf.checkDownloadStatus(forceDeleteIfDownloaded: false, transaction: transaction) } } + private func checkDownloadStatus( + forceDeleteIfDownloaded: Bool, + transaction: SDSAnyWriteTransaction + ) -> DownloadStatus { + guard + let rawStatus = kvStore.getData(Constants.kvStoreOnboardingStoryStatusKey, transaction: transaction), + let status = try? JSONDecoder().decode(DownloadStatus.self, from: rawStatus) + else { + return .requiresDownload + } + if status.isDownloaded { + // clean up opportunistically. + try? self.cleanUpStoriesIfNeeded( + messageUniqueIds: status.messageUniqueIds, + forceDeleteIfDownloaded: forceDeleteIfDownloaded, + transaction: transaction + ) + } + return status + } + private func cleanUpStoriesIfNeeded( messageUniqueIds: [String]?, + forceDeleteIfDownloaded: Bool, transaction: SDSAnyWriteTransaction ) throws { guard let messageUniqueIds = messageUniqueIds, !messageUniqueIds.isEmpty else { @@ -189,11 +248,18 @@ public class SystemStoryManager: NSObject, Dependencies, SystemStoryManagerProto guard !stories.isEmpty else { return } + + var shouldDelete = forceDeleteIfDownloaded + // If they exist and are expired, delete them. if let minViewTime = stories.lazy.compactMap(\.localUserViewedTimestamp).min(), Date().timeIntervalSince(Date(millisecondsSince1970: minViewTime)) >= Constants.postViewingTimeout { + shouldDelete = true + } + + if shouldDelete { stories.forEach { $0.sdsRemove(transaction: transaction) } @@ -305,13 +371,14 @@ public class SystemStoryManager: NSObject, Dependencies, SystemStoryManagerProto ) throws { try kvStore.setData( JSONEncoder().encode(DownloadStatus(messageUniqueIds: messageUniqueIds)), - key: Constants.kvStoreStatusKey, + key: Constants.kvStoreOnboardingStoryStatusKey, transaction: transaction ) } internal enum Constants { - static let kvStoreStatusKey = "OnboardingStoryStatus" + static let kvStoreOnboardingStoryStatusKey = "OnboardingStoryStatus" + static let kvStoreHiddenStateKey = "SystemStoriesAreHidden" static let manifestPath = "dynamic/ios/stories/onboarding/manifest.json" static let manifestVersionKey = "version" diff --git a/SignalServiceKit/src/Messages/Stories/SystemStoryManagerMock.swift b/SignalServiceKit/src/Messages/Stories/SystemStoryManagerMock.swift index 9ea90449fd..c2d0226476 100644 --- a/SignalServiceKit/src/Messages/Stories/SystemStoryManagerMock.swift +++ b/SignalServiceKit/src/Messages/Stories/SystemStoryManagerMock.swift @@ -24,6 +24,22 @@ public class SystemStoryManagerMock: NSObject, SystemStoryManagerProtocol { public func cleanUpOnboardingStoryIfNeeded() -> Promise { return cleanUpOnboardingStoryHandler() } + + public func addStateChangedObserver(_ observer: SystemStoryStateChangeObserver) { + fatalError("Unimplemented for tests") + } + + public func removeStateChangedObserver(_ observer: SystemStoryStateChangeObserver) { + fatalError("Unimplemented for tests") + } + + public func areSystemStoriesHidden(transaction: SDSAnyReadTransaction) -> Bool { + fatalError("Unimplemented for tests") + } + + public func setSystemStoriesHidden(_ hidden: Bool, transaction: SDSAnyWriteTransaction) { + fatalError("Unimplemented for tests") + } } public class OnboardingStoryManagerFilesystemMock: OnboardingStoryManagerFilesystem { diff --git a/SignalServiceKit/src/Messages/Stories/SystemStoryManagerProtocol.swift b/SignalServiceKit/src/Messages/Stories/SystemStoryManagerProtocol.swift index 75c4767df9..939da262c7 100644 --- a/SignalServiceKit/src/Messages/Stories/SystemStoryManagerProtocol.swift +++ b/SignalServiceKit/src/Messages/Stories/SystemStoryManagerProtocol.swift @@ -19,4 +19,20 @@ public protocol SystemStoryManagerProtocol: SystemStoryManagerProtocolObjc { /// to be expired, deletes it and cleans up references. /// Called on its own when the app is backgrounded. func cleanUpOnboardingStoryIfNeeded() -> Promise + + // MARK: Hidden State + + func addStateChangedObserver(_ observer: SystemStoryStateChangeObserver) + + func removeStateChangedObserver(_ observer: SystemStoryStateChangeObserver) + + func areSystemStoriesHidden(transaction: SDSAnyReadTransaction) -> Bool + + /// Sets system stories hidden state. If hiding, marks the onboarding story as viewed. + func setSystemStoriesHidden(_ hidden: Bool, transaction: SDSAnyWriteTransaction) +} + +public protocol SystemStoryStateChangeObserver: NSObject { + + func systemStoryHiddenStateDidChange(rowIds: [Int64]) } diff --git a/SignalUI/Views/Toast.swift b/SignalUI/Views/Toast.swift index 5274c7b945..10858a8551 100644 --- a/SignalUI/Views/Toast.swift +++ b/SignalUI/Views/Toast.swift @@ -35,9 +35,16 @@ public class ToastController: NSObject, ToastViewDelegate { owsAssertDebug(edge == .bottom || edge == .top) let offset = (edge == .top) ? inset : -inset + // Add to the first non-scrollview in the hierarchy, but still pin to the original view. + // We don't want the toast to be a subview of any scrollview or it will be subject to scrolling. + var parentView = view + while parentView is UIScrollView, let superview = view.superview { + parentView = superview + } + Logger.debug("") toastView.alpha = 0 - view.addSubview(toastView) + parentView.addSubview(toastView) toastView.setCompressionResistanceHigh() toastView.autoPinEdge(edge, to: edge, of: view, withOffset: offset) toastView.autoPinWidthToSuperview(withMargin: 8)