Story list search

* Hide navigation bar when searching in chats list

* Incomplete search bar layout

* apply story search filtering

* hide my stories section when searching

* Improve transitions for story tab bar search

* Show hidden stories in search results

* visual fixes to header when searching. include all unfiltered stories in viewer contexts

* delay getting the target story frame for interactive dismissal to give the view time to update after navigation bar changes

* fix strings

* pr feedback
This commit is contained in:
Harry 2022-10-24 15:38:54 -07:00 committed by GitHub
parent 023a83bb60
commit 08dabd70d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 352 additions and 90 deletions

View File

@ -665,6 +665,7 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup";
}
[self.searchResultsController viewWillAppear:animated];
[self ensureSearchBarCancelButton];
[self updateUnreadPaymentNotificationsCountWithSneakyTransaction];
@ -696,6 +697,7 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup";
self.isViewVisible = NO;
[self.searchResultsController viewWillDisappear:animated];
[self.navigationController setNavigationBarHidden:NO animated:animated];
}
- (void)updateLastViewedThread:(TSThread *)thread animated:(BOOL)animated
@ -769,10 +771,15 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup";
- (void)ensureSearchBarCancelButton
{
BOOL shouldShowCancelButton = (self.searchBar.isFirstResponder || self.searchBar.text.length > 0);
if (self.searchBar.showsCancelButton == shouldShowCancelButton) {
return;
BOOL shouldHideNavigationBar = shouldShowCancelButton && self.isViewVisible;
if (self.searchBar.showsCancelButton != shouldShowCancelButton) {
[self.searchBar setShowsCancelButton:shouldShowCancelButton animated:self.isViewVisible];
}
if (self.navigationController != nil) {
if (self.navigationController.isNavigationBarHidden != shouldHideNavigationBar) {
[self.navigationController setNavigationBarHidden:shouldHideNavigationBar animated: self.isViewVisible];
}
}
[self.searchBar setShowsCancelButton:shouldShowCancelButton animated:self.isViewVisible];
}
- (void)updateSearchResultsVisibility

View File

@ -13,6 +13,22 @@ import UIKit
class StoriesViewController: OWSViewController, StoryListDataSourceDelegate {
let tableView = UITableView()
let searchBarBackdropView = UIView()
let searchBarContainer = UIView()
let searchBar = OWSSearchBar()
var searchBarScrollingConstraint: NSLayoutConstraint?
var searchBarPinnedConstraint: NSLayoutConstraint?
var isFocusingSearchBar = false {
didSet {
searchBarBackdropView.isHidden = !isFocusingSearchBar
searchBarScrollingConstraint?.isActive = !isFocusingSearchBar
searchBarPinnedConstraint?.isActive = isFocusingSearchBar
}
}
private lazy var emptyStateLabel: UILabel = {
let label = UILabel()
label.textColor = Theme.secondaryTextAndIconColor
@ -40,19 +56,49 @@ class StoriesViewController: OWSViewController, StoryListDataSourceDelegate {
NotificationCenter.default.addObserver(self, selector: #selector(profileDidChange), name: .localProfileDidChange, object: nil)
}
override func loadView() {
view = tableView
tableView.delegate = self
tableView.dataSource = self
}
var tableViewIfLoaded: UITableView? {
return viewIfLoaded as? UITableView
return viewIfLoaded == nil ? nil : tableView
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.autoPinEdgesToSuperviewEdges()
tableView.delegate = self
tableView.dataSource = self
// Search
searchBarContainer.layoutMargins = .init(hMargin: 8, vMargin: 0)
// Comment is wrong but its the same string...
searchBar.placeholder = OWSLocalizedString(
"HOME_VIEW_CONVERSATION_SEARCHBAR_PLACEHOLDER",
comment: "Placeholder text for search bar which filters conversations."
)
searchBar.delegate = self
searchBar.sizeToFit()
searchBar.layoutMargins = .zero
searchBarContainer.frame = searchBar.frame
searchBarContainer.addSubview(searchBar)
searchBar.autoPinEdgesToSuperviewMargins()
let searchBarSizingView = UIView()
searchBarSizingView.frame = searchBarContainer.frame
self.tableView.tableHeaderView = searchBarSizingView
searchBarSizingView.addSubview(searchBarContainer)
searchBarContainer.autoPinHorizontalEdges(toEdgesOf: view)
self.searchBarScrollingConstraint = searchBarContainer.autoPinEdge(.top, to: .top, of: searchBarSizingView)
self.searchBarPinnedConstraint = searchBarContainer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
view.insertSubview(searchBarBackdropView, aboveSubview: tableView)
searchBarBackdropView.isHidden = true
searchBarBackdropView.backgroundColor = Theme.backgroundColor
searchBarBackdropView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom)
searchBarBackdropView.autoPinEdge(.bottom, to: .top, of: searchBarContainer)
title = NSLocalizedString("STORIES_TITLE", comment: "Title for the stories view.")
tableView.register(MyStoryCell.self, forCellReuseIdentifier: MyStoryCell.reuseIdentifier)
@ -89,11 +135,20 @@ class StoriesViewController: OWSViewController, StoryListDataSourceDelegate {
}
}
}
if isFocusingSearchBar {
navigationController?.setNavigationBarHidden(true, animated: false)
(tabBarController as? HomeTabBarController)?.setTabBarHidden(true, animated: false)
}
}
private var viewIsAppeared = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.viewIsAppeared = true
// Whether or not the theme has changed, always ensure
// the right theme is applied. The initial collapsed
// state of the split view controller is determined between
@ -117,8 +172,24 @@ class StoriesViewController: OWSViewController, StoryListDataSourceDelegate {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
defer {
self.viewIsAppeared = false
}
timestampUpdateTimer?.invalidate()
timestampUpdateTimer = nil
navigationController?.setNavigationBarHidden(false, animated: animated)
(tabBarController as? HomeTabBarController)?.setTabBarHidden(false, animated: animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if !searchBar.text.isEmptyOrNil {
searchBar.text = nil
dataSource.setSearchText(nil)
}
}
override func applyTheme() {
@ -155,6 +226,8 @@ class StoriesViewController: OWSViewController, StoryListDataSourceDelegate {
view.backgroundColor = Theme.backgroundColor
tableView.backgroundColor = Theme.backgroundColor
searchBarContainer.backgroundColor = Theme.backgroundColor
searchBarBackdropView.backgroundColor = Theme.backgroundColor
updateNavigationBar()
}
@ -276,9 +349,11 @@ class StoriesViewController: OWSViewController, StoryListDataSourceDelegate {
} else if
sectionConstraint ?? .hiddenStories == .hiddenStories,
let hiddenStoryIndex = dataSource.hiddenStories.firstIndex(where: { $0.context == context }),
!dataSource.isHiddenStoriesSectionCollapsed {
dataSource.shouldDisplayHiddenStories {
section = .hiddenStories
index = hiddenStoryIndex
// Offset for the header
let headerOffset = dataSource.shouldDisplayHiddenStoriesHeader ? 1 : 0
index = hiddenStoryIndex + headerOffset
} else {
// Not found.
return
@ -301,6 +376,13 @@ extension StoriesViewController: CameraFirstCaptureDelegate {
}
extension StoriesViewController: UITableViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if isFocusingSearchBar, searchBar.text?.isEmpty ?? true {
stopFocusingSearchBar(clearingSearchText: true)
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
@ -312,7 +394,7 @@ extension StoriesViewController: UITableViewDelegate {
navigationController?.pushViewController(MyStoriesViewController(), animated: true)
}
case .hiddenStories:
if indexPath.row == 0 {
if indexPath.row == 0, dataSource.shouldDisplayHiddenStoriesHeader {
// Tapping the collapsing header.
let wasCollapsed = dataSource.isHiddenStoriesSectionCollapsed
dataSource.isHiddenStoriesSectionCollapsed = !wasCollapsed
@ -433,7 +515,8 @@ extension StoriesViewController: UITableViewDataSource {
return dataSource.visibleStories[safe: indexPath.row]
case .hiddenStories:
// Offset by 1 to account for the header cell.
return dataSource.hiddenStories[safe: indexPath.row - 1]
let headerOffset = dataSource.shouldDisplayHiddenStoriesHeader ? 1 : 0
return dataSource.hiddenStories[safe: indexPath.row - headerOffset]
case .myStory, .none:
return nil
}
@ -448,10 +531,11 @@ extension StoriesViewController: UITableViewDataSource {
if let visibleRow = dataSource.visibleStories.firstIndex(where: { $0.context == context }) {
indexPath = IndexPath(row: visibleRow, section: Section.visibleStories.rawValue)
} else if
!dataSource.isHiddenStoriesSectionCollapsed,
dataSource.shouldDisplayHiddenStories,
let hiddenRow = dataSource.hiddenStories.firstIndex(where: { $0.context == context }) {
// Offset by 1 to account for the header cell.
indexPath = IndexPath(row: hiddenRow + 1, section: Section.hiddenStories.rawValue)
let headerOffset = dataSource.shouldDisplayHiddenStoriesHeader ? 1 : 0
indexPath = IndexPath(row: hiddenRow + headerOffset, section: Section.hiddenStories.rawValue)
} else {
return nil
}
@ -470,7 +554,7 @@ extension StoriesViewController: UITableViewDataSource {
cell.configure(with: myStoryModel) { [weak self] in self?.showCameraView() }
return cell
case .hiddenStories:
if indexPath.row == 0 {
if indexPath.row == 0 && dataSource.shouldDisplayHiddenStoriesHeader {
let cell = tableView.dequeueReusableCell(
withIdentifier: HiddenStoryHeaderCell.reuseIdentifier,
for: indexPath
@ -503,14 +587,15 @@ extension StoriesViewController: UITableViewDataSource {
switch Section(rawValue: section) {
case .myStory:
return dataSource.myStory == nil ? 0 : 1
return dataSource.shouldDisplayMyStory ? 1 : 0
case .visibleStories:
return dataSource.visibleStories.count
case .hiddenStories:
guard !dataSource.hiddenStories.isEmpty else {
return 0
}
return dataSource.isHiddenStoriesSectionCollapsed ? 1 : dataSource.hiddenStories.count + 1
return (
dataSource.shouldDisplayHiddenStoriesHeader ? 1 : 0
) + (
dataSource.shouldDisplayHiddenStories ? dataSource.hiddenStories.count : 0
)
case .none:
owsFailDebug("Unexpected section \(section)")
return 0
@ -518,6 +603,68 @@ extension StoriesViewController: UITableViewDataSource {
}
}
extension StoriesViewController: UISearchBarDelegate {
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
return viewIsAppeared
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
beginFocusingSearchBar()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
dataSource.setSearchText(searchText.isEmpty ? nil : searchText)
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
stopFocusingSearchBar(clearingSearchText: false)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
stopFocusingSearchBar(clearingSearchText: true)
}
private func beginFocusingSearchBar() {
self.navigationController?.setNavigationBarHidden(true, animated: true)
(tabBarController as? HomeTabBarController)?.setTabBarHidden(true, animated: true)
searchBar.setShowsCancelButton(true, animated: true)
// Do this as a transition animation so we get tighter timing
// with the navigation controller animation.
UIView.transition(
with: view,
duration: UINavigationController.hideShowBarDuration,
animations: {},
completion: { _ in
// Only change state when animations are done.
self.isFocusingSearchBar = true
}
)
}
private func stopFocusingSearchBar(clearingSearchText: Bool) {
self.navigationController?.setNavigationBarHidden(false, animated: true)
(tabBarController as? HomeTabBarController)?.setTabBarHidden(false, animated: true)
searchBar.setShowsCancelButton(false, animated: true)
// Do this as a transition animation so we get tighter timing
// with the navigation controller animation.
UIView.transition(
with: self.view,
duration: UINavigationController.hideShowBarDuration,
animations: {},
completion: { _ in
// Only change state when animations are done.
self.isFocusingSearchBar = false
}
)
searchBar.resignFirstResponder()
if clearingSearchText {
self.searchBar.text = nil
dataSource.setSearchText(nil)
}
}
}
extension StoriesViewController: StoryPageViewControllerDataSource {
func storyPageViewControllerAvailableContexts(
_ storyPageViewController: StoryPageViewController,

View File

@ -45,7 +45,7 @@ class StoryListDataSource: NSObject, Dependencies {
}
var allStories: [StoryViewModel] {
return syncingModels.exposedModel.stories
return syncingModels.exposedModel.unfilteredStories
}
var visibleStories: [StoryViewModel] {
@ -68,6 +68,14 @@ class StoryListDataSource: NSObject, Dependencies {
return syncingModels.threadSafeHiddenStoryContexts
}
public var shouldDisplayMyStory: Bool {
return syncingModels.exposedModel.shouldDisplayMyStory
}
public var shouldDisplayHiddenStoriesHeader: Bool {
return syncingModels.exposedModel.shouldDisplayHiddenStoriesHeader
}
public var isHiddenStoriesSectionCollapsed: Bool {
get {
return syncingModels.exposedModel.isHiddenStoriesSectionCollapsed
@ -77,25 +85,34 @@ class StoryListDataSource: NSObject, Dependencies {
}
}
public var shouldDisplayHiddenStories: Bool {
return syncingModels.exposedModel.shouldDisplayHiddenStories
}
public func setSearchText(_ text: String?) {
updateStoriesForSearchText(text)
}
// MARK: - Reload
func reloadStories() {
loadingQueue.async {
self.syncingModels.mutate { oldModel -> StoryChanges? in
let (listStories, outgoingStories) = Self.databaseStorage.read {
(
StoryFinder.storiesForListView(transaction: $0),
StoryFinder.outgoingStories(transaction: $0)
)
let (listStories, myStoryModel) = Self.databaseStorage.read { transaction -> ([StoryViewModel], MyStoryViewModel) in
let storiesForList = StoryFinder.storiesForListView(transaction: transaction)
let groupedMessages = Dictionary(grouping: storiesForList) { $0.context }
let storyListModels = groupedMessages.compactMap { _, messages -> StoryViewModel? in
return try? StoryViewModel(messages: messages, transaction: transaction)
}
let outgoingStories = StoryFinder.outgoingStories(transaction: transaction)
let myStoryModel = MyStoryViewModel(messages: outgoingStories, transaction: transaction)
return (storyListModels, myStoryModel)
}
let myStoryModel = Self.databaseStorage.read { MyStoryViewModel(messages: outgoingStories, transaction: $0) }
let groupedMessages = Dictionary(grouping: listStories) { $0.context }
let newValues = Self.databaseStorage.read { transaction in
groupedMessages.compactMap { try? StoryViewModel(messages: $1, transaction: transaction) }
}.sorted(by: Self.sortStoryModels)
let newModel = StoryListViewModel(
myStory: myStoryModel,
stories: newValues,
stories: listStories.sorted(by: Self.sortStoryModels),
searchText: oldModel.searchText,
isHiddenStoriesSectionCollapsed: oldModel.isHiddenStoriesSectionCollapsed
)
// Note everything but the new model gets ignored since we reload data
@ -171,6 +188,32 @@ class StoryListDataSource: NSObject, Dependencies {
}
}
private func updateStoriesForSearchText(_ searchText: String?) {
loadingQueue.async {
self.syncingModels.mutate { oldModel -> StoryChanges? in
let newModel = StoryListViewModel(
myStory: oldModel.myStory,
stories: oldModel.unfilteredStories,
searchText: searchText,
isHiddenStoriesSectionCollapsed: oldModel.isHiddenStoriesSectionCollapsed
)
// Note everything but the new model gets ignored since we reload data
// rather than apply individual row updates.
return StoryChanges(
oldModel: oldModel,
newModel: newModel,
visibleStoryUpdates: [], // is ignored
hiddenStoryUpdates: [], // is ignored
myStoryChanged: true // is ignored
)
} sync: { _ in
self.tableView?.reloadData()
self.observeAssociatedDataChangesForAvailableModels()
self.delegate?.tableViewDidUpdate()
}
}
}
private func updateStories(_ mutate: @escaping (StoryListViewModel) -> StoryChanges?) {
AssertIsOnMainThread()
@ -335,7 +378,7 @@ class StoryListDataSource: NSObject, Dependencies {
var changedHiddenContexts = [StoryContext]()
let newModels = try Self.databaseStorage.read { transaction -> [StoryViewModel] in
let changedModels = try oldViewModel.stories.compactMap { oldModel -> StoryViewModel? in
let changedModels = try oldViewModel.unfilteredStories.compactMap { oldModel -> StoryViewModel? in
guard let latestMessage = oldModel.messages.first else { return oldModel }
let modelDeletedRowIds: [Int64] = oldModel.messages.lazy.compactMap(\.id).filter { deletedRowIds.contains($0) }
@ -391,7 +434,7 @@ class StoryListDataSource: NSObject, Dependencies {
let newIsHiddenStoriesSectionCollapsed: Bool
if !oldViewModel.isHiddenStoriesSectionCollapsed {
newIsHiddenStoriesSectionCollapsed = false
} else if oldViewModel.hiddenStories.isEmpty && newModels.contains(where: \.isHidden) {
} else if oldViewModel.unfilteredHiddenStories.isEmpty && newModels.contains(where: \.isHidden) {
newIsHiddenStoriesSectionCollapsed = false
} else {
newIsHiddenStoriesSectionCollapsed = true
@ -400,6 +443,7 @@ class StoryListDataSource: NSObject, Dependencies {
let newViewModel = StoryListViewModel(
myStory: myStoryModel ?? oldViewModel.myStory,
stories: newModels,
searchText: oldViewModel.searchText,
isHiddenStoriesSectionCollapsed: newIsHiddenStoriesSectionCollapsed
)
@ -482,13 +526,12 @@ class StoryListDataSource: NSObject, Dependencies {
self.delegate?.tableViewDidUpdate()
}
if changes.oldModel.myStory == nil, changes.newModel.myStory != nil {
if !changes.oldModel.shouldDisplayMyStory, changes.newModel.shouldDisplayMyStory {
tableView.insertRows(at: [IndexPath(row: 0, section: Section.myStory.rawValue)], with: .fade)
} else if changes.oldModel.myStory != nil, changes.newModel.myStory == nil {
// My story should never go away after being loaded, but for the sake of completeness...
} else if changes.oldModel.shouldDisplayMyStory, !changes.newModel.shouldDisplayMyStory {
tableView.deleteRows(at: [IndexPath(row: 0, section: Section.myStory.rawValue)], with: .fade)
} else if changes.myStoryChanged {
tableView.reloadRows(at: [IndexPath(row: 0, section: Section.myStory.rawValue)], with: .none)
} else if changes.myStoryChanged, changes.newModel.shouldDisplayMyStory {
tableView.reloadRows(at: [IndexPath(row: 0, section: Section.myStory.rawValue)], with: .fade)
}
// Visible stories section is always visible, directly apply changes.
@ -503,13 +546,14 @@ class StoryListDataSource: NSObject, Dependencies {
return
}
switch (changes.oldModel.hiddenStories.isEmpty, changes.newModel.hiddenStories.isEmpty) {
case (true, true):
// No need to do anything.
return
// Header
switch (changes.oldModel.shouldDisplayHiddenStoriesHeader, changes.newModel.shouldDisplayHiddenStoriesHeader) {
case (false, false):
// No need to do anything.
break
case (true, true):
// Just reload the header row if we have to.
if changes.oldModel.isHiddenStoriesSectionCollapsed != changes.newModel.isHiddenStoriesSectionCollapsed {
// If the cell is visible, reconfigure it directly without reloading.
@ -523,68 +567,84 @@ class StoryListDataSource: NSObject, Dependencies {
tableView.reloadRows(at: [path], with: .none)
}
}
case (true, false):
tableView.insertRows(at: [IndexPath(row: 0, section: Section.hiddenStories.rawValue)], with: .fade)
case (false, true):
tableView.insertRows(at: [IndexPath(row: 0, section: Section.hiddenStories.rawValue)], with: .fade)
case (true, false):
tableView.deleteRows(at: [IndexPath(row: 0, section: Section.hiddenStories.rawValue)], with: .fade)
applyTableViewBatchUpdates(
changes.oldModel.hiddenStories.lazy.enumerated().map {
// Offset by 1 to account for the header cell.
return .init(value: $1.context, updateType: .delete(oldIndex: $0 + 1))
},
toSection: .hiddenStories,
models: changes.oldModel.hiddenStories
)
return
}
switch (changes.oldModel.isHiddenStoriesSectionCollapsed, changes.newModel.isHiddenStoriesSectionCollapsed) {
// Offset by 1 to account for the header cell.
let oldHeaderOffset = changes.oldModel.shouldDisplayHiddenStoriesHeader ? 1 : 0
let newHeaderOffset = changes.newModel.shouldDisplayHiddenStoriesHeader ? 1 : 0
case (false, false):
// Update the hidden section, it was expanded before and after
switch (changes.oldModel.shouldDisplayHiddenStories, changes.newModel.shouldDisplayHiddenStories) {
case (true, true):
// Update the hidden section, it was shown before and after
applyTableViewBatchUpdates(
changes.hiddenStoryUpdates.map {
// Offset by 1 to account for the header cell.
switch $0.updateType {
case let .update(oldIndex, newIndex):
return .init(value: $0.value, updateType: .update(oldIndex: oldIndex + 1, newIndex: newIndex + 1))
return .init(
value: $0.value,
updateType: .update(
oldIndex: oldIndex + oldHeaderOffset,
newIndex: newIndex + newHeaderOffset
)
)
case let .move(oldIndex, newIndex):
return .init(value: $0.value, updateType: .move(oldIndex: oldIndex + 1, newIndex: newIndex + 1))
return .init(
value: $0.value,
updateType: .move(
oldIndex: oldIndex + oldHeaderOffset,
newIndex: newIndex + newHeaderOffset
)
)
case let .insert(newIndex):
return .init(value: $0.value, updateType: .insert(newIndex: newIndex + 1))
return .init(
value: $0.value,
updateType: .insert(
newIndex: newIndex + newHeaderOffset
)
)
case let .delete(oldIndex):
return .init(value: $0.value, updateType: .delete(oldIndex: oldIndex + 1))
return .init(
value: $0.value,
updateType: .delete(
oldIndex: oldIndex + oldHeaderOffset
)
)
}
},
toSection: .hiddenStories,
// Offset by 1 to account for the header cell.
models: [changes.newModel.hiddenStories.first].compactMap({ $0 }) + changes.newModel.hiddenStories
models: [changes.newModel.hiddenStories.first].compactMap({
changes.newModel.shouldDisplayHiddenStoriesHeader ? $0 : nil
}) + changes.newModel.hiddenStories
)
case (true, false):
// Was collapsed and is now expanded, reload.
case (false, true):
// Was hidden and is now shown, reload.
applyTableViewBatchUpdates(
changes.newModel.hiddenStories.lazy.enumerated().map {
// Offset by 1 to account for the header cell.
return .init(value: $1.context, updateType: .insert(newIndex: $0 + 1))
return .init(value: $1.context, updateType: .insert(newIndex: $0 + newHeaderOffset))
},
toSection: .hiddenStories,
models: changes.newModel.hiddenStories
)
case (false, true):
// Was expanded and is now collapsed, everything counts as a delete.
case (true, false):
// Was shown and is now hidden, everything counts as a delete.
applyTableViewBatchUpdates(
changes.oldModel.hiddenStories.lazy.enumerated().map {
// Offset by 1 to account for the header cell.
return .init(value: $1.context, updateType: .delete(oldIndex: $0 + 1))
return .init(value: $1.context, updateType: .delete(oldIndex: $0 + oldHeaderOffset))
},
toSection: .hiddenStories,
models: changes.oldModel.hiddenStories
)
case (true, true):
// Was collapsed and is collapsed, so can just ignore any updates.
case (false, false):
// Was hidden and is hidden, so can just ignore any updates.
break
}
}
@ -717,28 +777,75 @@ private class SyncingStoryListViewModel {
private struct StoryListViewModel {
let myStory: MyStoryViewModel?
let stories: [StoryViewModel]
let unfilteredStories: [StoryViewModel]
let searchText: String?
let isHiddenStoriesSectionCollapsed: Bool
let stories: [StoryViewModel]
init(
myStory: MyStoryViewModel?,
stories: [StoryViewModel],
searchText: String?,
isHiddenStoriesSectionCollapsed: Bool
) {
self.myStory = myStory
self.unfilteredStories = stories
self.searchText = searchText
self.isHiddenStoriesSectionCollapsed = isHiddenStoriesSectionCollapsed
self.stories = {
guard let searchText = searchText else {
return stories
}
return stories.filter {
$0.latestMessageName.localizedCaseInsensitiveContains(searchText)
}
}()
}
static var empty: Self {
return .init(myStory: nil, stories: [], isHiddenStoriesSectionCollapsed: true)
return .init(myStory: nil, stories: [], searchText: nil, isHiddenStoriesSectionCollapsed: true)
}
func copy(isHiddenStoriesSectionCollapsed: Bool) -> Self {
return .init(
myStory: myStory,
stories: stories,
stories: unfilteredStories,
searchText: searchText,
isHiddenStoriesSectionCollapsed: isHiddenStoriesSectionCollapsed
)
}
var shouldDisplayMyStory: Bool {
return myStory != nil && searchText.isEmptyOrNil
}
var visibleStories: [StoryViewModel] {
return stories.lazy.filter(\.isHidden.negated)
}
// MARK: Hidden stories
var hiddenStories: [StoryViewModel] {
return stories.lazy.filter(\.isHidden)
}
var unfilteredHiddenStories: [StoryViewModel] {
return unfilteredStories.lazy.filter(\.isHidden)
}
var shouldDisplayHiddenStoriesHeader: Bool {
return !hiddenStories.isEmpty && searchText.isEmptyOrNil
}
var shouldDisplayHiddenStories: Bool {
if shouldDisplayHiddenStoriesHeader {
return !isHiddenStoriesSectionCollapsed
} else {
return !hiddenStories.isEmpty
}
}
}
/// Encapsulates a set of changes to be applied when story list state changes.

View File

@ -48,12 +48,13 @@ class StoryZoomAnimator: NSObject, UIViewControllerAnimatedTransitioning {
return
}
let centerCroppedDismissedFrame = containerView.convert(
context.thumbnailView.frame,
from: context.thumbnailView.superview
)
var storyViewDismissedSize = centerCroppedDismissedFrame.size
let getCenterCroppedDismissedFrame = {
return containerView.convert(
self.context.thumbnailView.frame,
from: self.context.thumbnailView.superview
)
}
var storyViewDismissedSize = getCenterCroppedDismissedFrame().size
// The thumbnailView uses contentMode scaleAspectFill to center crop the thumbnail,
// but the storyView uses contentMode scaleAspectFit to letter box the full screen
@ -63,7 +64,7 @@ class StoryZoomAnimator: NSObject, UIViewControllerAnimatedTransitioning {
// and then crop it ourselves within storyViewContainer.
if let storyThumbnailSize = context.storyThumbnailSize {
if storyThumbnailSize.height > storyThumbnailSize.width {
if storyThumbnailSize.aspectRatio > centerCroppedDismissedFrame.size.aspectRatio {
if storyThumbnailSize.aspectRatio > storyViewDismissedSize.aspectRatio {
storyViewDismissedSize.width = storyViewDismissedSize.height * storyThumbnailSize.aspectRatio
}
@ -88,7 +89,7 @@ class StoryZoomAnimator: NSObject, UIViewControllerAnimatedTransitioning {
containerView.addSubview(storyViewContainer)
containerView.addSubview(toVC.view)
storyViewContainer.frame = centerCroppedDismissedFrame
storyViewContainer.frame = getCenterCroppedDismissedFrame()
context.storyView.frame = storyViewContainer.bounds.centerCropping(fullSize: storyViewDismissedSize)
context.storyView.layoutIfNeeded()
@ -150,7 +151,7 @@ class StoryZoomAnimator: NSObject, UIViewControllerAnimatedTransitioning {
storyView.renderSize = TextAttachmentThumbnailView.defaultRenderSize
}
self.storyViewContainer.layer.cornerRadius = 12
self.storyViewContainer.frame = centerCroppedDismissedFrame
self.storyViewContainer.frame = getCenterCroppedDismissedFrame()
self.context.storyView.frame = self.storyViewContainer.bounds.centerCropping(fullSize: storyViewDismissedSize)
self.context.storyView.layoutIfNeeded()
}
@ -192,7 +193,7 @@ class StoryZoomAnimator: NSObject, UIViewControllerAnimatedTransitioning {
self.animateChromeFade()
self.animatePresentation(
delay: self.presentationDelay,
endFrame: centerCroppedDismissedFrame,
endFrame: getCenterCroppedDismissedFrame(),
storyViewEndSize: storyViewDismissedSize
)
self.animateThumbnailFade(delay: self.presentationDuration + self.crossFadeDuration)