diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index ef585a4bd9..80915ef9ad 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -956,6 +956,7 @@ 88B00D4B28A32DB600BC9CA0 /* StoryGroupReplyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B00D4A28A32DB600BC9CA0 /* StoryGroupReplyViewController.swift */; }; 88B00D4D28A3346100BC9CA0 /* StoryViewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B00D4C28A3346000BC9CA0 /* StoryViewsViewController.swift */; }; 88B00D4F28A33B5800BC9CA0 /* StoryPrivateViewsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B00D4E28A33B5800BC9CA0 /* StoryPrivateViewsSheet.swift */; }; + 88B00D5128A341D000BC9CA0 /* StoryGroupRepliesAndViewsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B00D5028A341CF00BC9CA0 /* StoryGroupRepliesAndViewsSheet.swift */; }; 88B2234A283F290400A25048 /* StoryPrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B22349283F290400A25048 /* StoryPrivacySettingsViewController.swift */; }; 88B2234C284FABE600A25048 /* StoryThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B2234B284FABE600A25048 /* StoryThumbnailView.swift */; }; 88B688B0238F0D1000286F82 /* ReactionsDetailSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B688AF238F0D1000286F82 /* ReactionsDetailSheet.swift */; }; @@ -3281,6 +3282,7 @@ 88B00D4A28A32DB600BC9CA0 /* StoryGroupReplyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyViewController.swift; sourceTree = ""; }; 88B00D4C28A3346000BC9CA0 /* StoryViewsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryViewsViewController.swift; sourceTree = ""; }; 88B00D4E28A33B5800BC9CA0 /* StoryPrivateViewsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryPrivateViewsSheet.swift; sourceTree = ""; }; + 88B00D5028A341CF00BC9CA0 /* StoryGroupRepliesAndViewsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupRepliesAndViewsSheet.swift; sourceTree = ""; }; 88B22349283F290400A25048 /* StoryPrivacySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryPrivacySettingsViewController.swift; sourceTree = ""; }; 88B2234B284FABE600A25048 /* StoryThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryThumbnailView.swift; sourceTree = ""; }; 88B688AF238F0D1000286F82 /* ReactionsDetailSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsDetailSheet.swift; sourceTree = ""; }; @@ -6104,6 +6106,7 @@ isa = PBXGroup; children = ( 8864072727EEA658009916B6 /* StoryGroupReplySheet.swift */, + 88B00D5028A341CF00BC9CA0 /* StoryGroupRepliesAndViewsSheet.swift */, 88B00D4A28A32DB600BC9CA0 /* StoryGroupReplyViewController.swift */, 8864072927F0D426009916B6 /* StoryGroupReplyLoader.swift */, 8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */, @@ -10284,6 +10287,7 @@ 3428577926BD8777005A2A96 /* Emoji+SkinTones.swift in Sources */, 329D42A225AD65C400DCB449 /* GetStartedBannerCell.swift in Sources */, 348815C9255346A500D4F4C4 /* CVComponent.swift in Sources */, + 88B00D5128A341D000BC9CA0 /* StoryGroupRepliesAndViewsSheet.swift in Sources */, 32AC5CE7255B51E900829BD8 /* JoinGroupCallPill.swift in Sources */, 32ECD08A24BFBF9500EDDED0 /* ContactSupportViewController.swift in Sources */, 88A4CC10246CE2760082211F /* TransferProgressView.swift in Sources */, diff --git a/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryContextViewController.swift b/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryContextViewController.swift index 806b78d94d..5529c409ce 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryContextViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/Context View/StoryContextViewController.swift @@ -491,11 +491,20 @@ extension StoryContextViewController: UIGestureRecognizerDelegate { switch self.context { case .groupId: - let groupReplyVC = StoryGroupReplySheet(storyMessage: currentItem.message) - groupReplyVC.interactiveTransitionCoordinator = interactiveTransitionCoordinator - groupReplyVC.dismissHandler = { [weak self] in self?.play() } - self.pause() - self.present(groupReplyVC, animated: true) + switch currentItem.message.direction { + case .outgoing: + let groupRepliesAndViewsVC = StoryGroupRepliesAndViewsSheet(storyMessage: currentItem.message) + groupRepliesAndViewsVC.interactiveTransitionCoordinator = interactiveTransitionCoordinator + groupRepliesAndViewsVC.dismissHandler = { [weak self] in self?.play() } + self.pause() + self.present(groupRepliesAndViewsVC, animated: true) + case .incoming: + let groupReplyVC = StoryGroupReplySheet(storyMessage: currentItem.message) + groupReplyVC.interactiveTransitionCoordinator = interactiveTransitionCoordinator + groupReplyVC.dismissHandler = { [weak self] in self?.play() } + self.pause() + self.present(groupReplyVC, animated: true) + } case .authorUuid: let directReplyVC = StoryDirectReplySheet(storyMessage: currentItem.message) directReplyVC.interactiveTransitionCoordinator = interactiveTransitionCoordinator diff --git a/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/Group Reply Sheet/StoryGroupRepliesAndViewsSheet.swift b/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/Group Reply Sheet/StoryGroupRepliesAndViewsSheet.swift new file mode 100644 index 0000000000..dcaca6869d --- /dev/null +++ b/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/Group Reply Sheet/StoryGroupRepliesAndViewsSheet.swift @@ -0,0 +1,192 @@ +// +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. +// + +import Foundation +import UIKit +import SignalServiceKit +import SignalUI + +class StoryGroupRepliesAndViewsSheet: InteractiveSheetViewController { + override var interactiveScrollViews: [UIScrollView] { [groupReplyViewController.tableView, viewsViewController.tableView] } + override var minHeight: CGFloat { CurrentAppContext().frame.height * 0.6 } + override var sheetBackgroundColor: UIColor { .ows_gray90 } + + weak var interactiveTransitionCoordinator: StoryInteractiveTransitionCoordinator? + private let groupReplyViewController: StoryGroupReplyViewController + private let viewsViewController: StoryViewsViewController + private let pagingScrollView = UIScrollView() + + private lazy var viewsButton = createToggleButton( + title: NSLocalizedString("STORIES_VIEWS_TAB", comment: "Title text for the 'views' tab on the stories views & replies sheet") + ) { [weak self] in + self?.switchToViewsTab(animated: true) + } + + private lazy var repliesButton = createToggleButton( + title: NSLocalizedString("STORIES_REPLIES_TAB", comment: "Title text for the 'replies' tab on the stories views & replies sheet") + ) { [weak self] in + self?.switchToRepliesTab(animated: true) + } + + var dismissHandler: (() -> Void)? + + init(storyMessage: StoryMessage) { + self.groupReplyViewController = StoryGroupReplyViewController(storyMessage: storyMessage) + self.viewsViewController = StoryViewsViewController(storyMessage: storyMessage) + + super.init() + } + + public required init() { + fatalError("init() has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + let vStack = UIStackView() + vStack.axis = .vertical + vStack.alignment = .center + vStack.spacing = 20 + contentView.addSubview(vStack) + vStack.autoPinEdgesToSuperviewEdges() + + let hStack = UIStackView(arrangedSubviews: [viewsButton, repliesButton]) + hStack.axis = .horizontal + hStack.spacing = 12 + vStack.addArrangedSubview(hStack) + + pagingScrollView.isPagingEnabled = true + pagingScrollView.showsHorizontalScrollIndicator = false + pagingScrollView.isDirectionalLockEnabled = true + pagingScrollView.delegate = self + vStack.addArrangedSubview(pagingScrollView) + pagingScrollView.autoPinEdge(toSuperviewSafeArea: .left) + pagingScrollView.autoPinEdge(toSuperviewSafeArea: .right) + + let pagesContainer = UIView() + pagingScrollView.addSubview(pagesContainer) + pagesContainer.autoPinEdgesToSuperviewEdges() + pagesContainer.autoMatch(.height, to: .height, of: pagingScrollView) + pagesContainer.autoMatch(.width, to: .width, of: pagingScrollView, withMultiplier: 2) + + addChild(viewsViewController) + pagesContainer.addSubview(viewsViewController.view) + viewsViewController.view.autoMatch(.width, to: .width, of: pagesContainer, withMultiplier: 0.5) + viewsViewController.view.autoPinHeightToSuperview() + viewsViewController.view.autoPinEdge(toSuperviewEdge: .leading) + + groupReplyViewController.delegate = self + addChild(groupReplyViewController) + pagesContainer.addSubview(groupReplyViewController.view) + groupReplyViewController.view.autoMatch(.width, to: .width, of: pagesContainer, withMultiplier: 0.5) + groupReplyViewController.view.autoPinHeightToSuperview() + groupReplyViewController.view.autoPinEdge(.leading, to: .trailing, of: viewsViewController.view) + groupReplyViewController.view.autoPinEdge(toSuperviewEdge: .trailing) + + switchToViewsTab(animated: false) + } + + override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + super.dismiss(animated: flag) { [dismissHandler] in + completion?() + dismissHandler?() + } + } + + private var isManuallySwitchingTabs = false + func switchToRepliesTab(animated: Bool) { + isManuallySwitchingTabs = true + repliesButton.isSelected = true + viewsButton.isSelected = false + pagingScrollView.setContentOffset(CGPoint(x: pagingScrollView.width, y: 0), animated: animated) + isManuallySwitchingTabs = false + } + + func switchToViewsTab(animated: Bool) { + isManuallySwitchingTabs = true + repliesButton.isSelected = false + viewsButton.isSelected = true + pagingScrollView.setContentOffset(.zero, animated: animated) + isManuallySwitchingTabs = false + } + + func createToggleButton(title: String, block: @escaping () -> Void) -> UIButton { + let button = OWSButton() + button.block = { + guard !button.isSelected else { return } + block() + } + button.autoSetDimension(.height, toSize: 28) + button.layer.cornerRadius = 14 + button.clipsToBounds = true + button.titleLabel?.font = UIFont.ows_semiboldFont(withSize: 15) + button.contentEdgeInsets = UIEdgeInsets(hMargin: 12, vMargin: 4) + button.setTitle(title, for: .normal) + button.setTitleColor(Theme.darkThemePrimaryColor, for: .normal) + button.setBackgroundImage(UIImage(color: .ows_gray65), for: .selected) + return button + } +} + +extension StoryGroupRepliesAndViewsSheet: UIScrollViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + groupReplyViewController.inputToolbar.resignFirstResponder() + + guard !isManuallySwitchingTabs else { return } + + if scrollView.contentOffset.x < scrollView.width / 2 { + repliesButton.isSelected = false + viewsButton.isSelected = true + } else { + repliesButton.isSelected = true + viewsButton.isSelected = false + } + } +} + +extension StoryGroupRepliesAndViewsSheet: StoryGroupReplyDelegate { + func storyGroupReplyViewControllerDidBeginEditing(_ storyGroupReplyViewController: StoryGroupReplyViewController) { + maximizeHeight() + } +} + +extension StoryGroupRepliesAndViewsSheet { + override func presentationController( + forPresented presented: UIViewController, + presenting: UIViewController?, + source: UIViewController + ) -> UIPresentationController? { + return nil + } + + public func animationController( + forPresented presented: UIViewController, + presenting: UIViewController, + source: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + return StoryReplySheetAnimator( + isPresenting: true, + isInteractive: interactiveTransitionCoordinator != nil, + backdropView: backdropView + ) + } + + public func animationController( + forDismissed dismissed: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + return StoryReplySheetAnimator( + isPresenting: false, + isInteractive: false, + backdropView: backdropView + ) + } + + public func interactionControllerForPresentation( + using animator: UIViewControllerAnimatedTransitioning + ) -> UIViewControllerInteractiveTransitioning? { + interactiveTransitionCoordinator?.mode = .reply + return interactiveTransitionCoordinator + } +} diff --git a/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/Group Reply Sheet/StoryGroupReplyViewController.swift b/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/Group Reply Sheet/StoryGroupReplyViewController.swift index fe55511653..c4770fc2ef 100644 --- a/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/Group Reply Sheet/StoryGroupReplyViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Stories/Replies & Views Sheets/Group Reply Sheet/StoryGroupReplyViewController.swift @@ -6,7 +6,13 @@ import Foundation import UIKit import SignalServiceKit +protocol StoryGroupReplyDelegate: AnyObject { + func storyGroupReplyViewControllerDidBeginEditing(_ storyGroupReplyViewController: StoryGroupReplyViewController) +} + class StoryGroupReplyViewController: OWSViewController, StoryReplySheet { + weak var delegate: StoryGroupReplyDelegate? + private(set) lazy var tableView = UITableView() private(set) lazy var inputToolbar = StoryReplyInputToolbar() private lazy var inputToolbarBottomConstraint = inputToolbar.autoPinEdge(toSuperviewEdge: .bottom) @@ -193,8 +199,7 @@ extension StoryGroupReplyViewController: InputAccessoryViewPlaceholderDelegate { extension StoryGroupReplyViewController: StoryReplyInputToolbarDelegate { func storyReplyInputToolbarDidBeginEditing(_ storyReplyInputToolbar: StoryReplyInputToolbar) { -// maximizeHeight() - // TODO: + delegate?.storyGroupReplyViewControllerDidBeginEditing(self) } func storyReplyInputToolbarHeightDidChange(_ storyReplyInputToolbar: StoryReplyInputToolbar) { diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 8f54cb4502..5ff9934f90 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -6154,6 +6154,9 @@ /* Toggle text for the 'replies & reactions' switch in stories settings */ "STORIES_REPLIES_AND_REACTIONS_TOGGLE" = "Allow Replies & Reactions"; +/* Title text for the 'replies' tab on the stories views & replies sheet */ +"STORIES_REPLIES_TAB" = "Replies"; + /* alert notifying that the 'save' operation failed */ "STORIES_SAVE_FAILED" = "Failed to Save Story"; @@ -6178,6 +6181,9 @@ /* Title for the stories view. */ "STORIES_TITLE" = "Stories"; +/* Title text for the 'views' tab on the stories views & replies sheet */ +"STORIES_VIEWS_TAB" = "Views"; + /* One of two possible sources when composing a new story. Displayed at the bottom in in-app camera. */ "STORY_COMPOSER_CAMERA" = "Camera";