forked from zelo72/mastodon-ios
feat: make reply to foldable in compose scene
This commit is contained in:
parent
83904126dd
commit
ccb74b08f7
|
@ -34,6 +34,7 @@ extension ComposeStatusSection {
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
managedObjectContext: NSManagedObjectContext,
|
managedObjectContext: NSManagedObjectContext,
|
||||||
composeKind: ComposeKind,
|
composeKind: ComposeKind,
|
||||||
|
repliedToCellFrameSubscriber: CurrentValueSubject<CGRect, Never>,
|
||||||
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
|
||||||
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
|
||||||
|
@ -70,6 +71,8 @@ extension ComposeStatusSection {
|
||||||
cell.statusView.activeTextLabel.configure(content: status.content)
|
cell.statusView.activeTextLabel.configure(content: status.content)
|
||||||
// set date
|
// set date
|
||||||
cell.statusView.dateLabel.text = status.createdAt.shortTimeAgoSinceNow
|
cell.statusView.dateLabel.text = status.createdAt.shortTimeAgoSinceNow
|
||||||
|
|
||||||
|
cell.framePublisher.assign(to: \.value, on: repliedToCellFrameSubscriber).store(in: &cell.disposeBag)
|
||||||
}
|
}
|
||||||
return cell
|
return cell
|
||||||
case .input(let replyToStatusObjectID, let attribute):
|
case .input(let replyToStatusObjectID, let attribute):
|
||||||
|
|
|
@ -14,6 +14,8 @@ final class ComposeRepliedToStatusContentCollectionViewCell: UICollectionViewCel
|
||||||
|
|
||||||
let statusView = StatusView()
|
let statusView = StatusView()
|
||||||
|
|
||||||
|
let framePublisher = PassthroughSubject<CGRect, Never>()
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
@ -32,6 +34,11 @@ final class ComposeRepliedToStatusContentCollectionViewCell: UICollectionViewCel
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
framePublisher.send(bounds)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeRepliedToStatusContentCollectionViewCell {
|
extension ComposeRepliedToStatusContentCollectionViewCell {
|
||||||
|
|
|
@ -31,7 +31,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.normal.color.withAlphaComponent(0.5)), for: .highlighted)
|
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.normal.color.withAlphaComponent(0.5)), for: .highlighted)
|
||||||
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
|
||||||
button.setTitleColor(.white, for: .normal)
|
button.setTitleColor(.white, for: .normal)
|
||||||
button.contentEdgeInsets = UIEdgeInsets(top: 5.5, left: 16, bottom: 5.5, right: 16) // set 28pt height
|
button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height
|
||||||
button.adjustsImageWhenHighlighted = false
|
button.adjustsImageWhenHighlighted = false
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
@ -50,6 +50,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self))
|
collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self))
|
||||||
collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self))
|
collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self))
|
||||||
collectionView.backgroundColor = Asset.Scene.Compose.background.color
|
collectionView.backgroundColor = Asset.Scene.Compose.background.color
|
||||||
|
collectionView.alwaysBounceVertical = true
|
||||||
return collectionView
|
return collectionView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -331,6 +332,24 @@ extension ComposeViewController {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// setup snap behavior
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
viewModel.repliedToCellFrame.removeDuplicates().eraseToAnyPublisher(),
|
||||||
|
viewModel.collectionViewState.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] repliedToCellFrame, collectionViewState in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard repliedToCellFrame != .zero else { return }
|
||||||
|
switch collectionViewState {
|
||||||
|
case .fold:
|
||||||
|
self.collectionView.contentInset.top = -repliedToCellFrame.height
|
||||||
|
case .expand:
|
||||||
|
self.collectionView.contentInset.top = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -754,6 +773,32 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - UIScrollViewDelegate
|
||||||
|
extension ComposeViewController {
|
||||||
|
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||||
|
guard scrollView === collectionView else { return }
|
||||||
|
|
||||||
|
let repliedToCellFrame = viewModel.repliedToCellFrame.value
|
||||||
|
guard repliedToCellFrame != .zero else { return }
|
||||||
|
let throttle = viewModel.repliedToCellFrame.value.height - scrollView.adjustedContentInset.top
|
||||||
|
// print("\(throttle) - \(scrollView.contentOffset.y)")
|
||||||
|
|
||||||
|
switch viewModel.collectionViewState.value {
|
||||||
|
case .fold:
|
||||||
|
if scrollView.contentOffset.y < throttle {
|
||||||
|
viewModel.collectionViewState.value = .expand
|
||||||
|
}
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
|
||||||
|
case .expand:
|
||||||
|
if scrollView.contentOffset.y > -44 {
|
||||||
|
viewModel.collectionViewState.value = .fold
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
extension ComposeViewController: UICollectionViewDelegate {
|
extension ComposeViewController: UICollectionViewDelegate {
|
||||||
|
|
||||||
|
@ -790,6 +835,10 @@ extension ComposeViewController: UICollectionViewDelegate {
|
||||||
|
|
||||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||||
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
|
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
|
||||||
|
|
||||||
|
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||||
|
return .fullScreen
|
||||||
|
}
|
||||||
|
|
||||||
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||||
return viewModel.shouldDismiss.value
|
return viewModel.shouldDismiss.value
|
||||||
|
|
|
@ -27,6 +27,7 @@ extension ComposeViewModel {
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
managedObjectContext: context.managedObjectContext,
|
managedObjectContext: context.managedObjectContext,
|
||||||
composeKind: composeKind,
|
composeKind: composeKind,
|
||||||
|
repliedToCellFrameSubscriber: repliedToCellFrame,
|
||||||
customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
|
customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
|
||||||
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
|
||||||
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
|
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
|
||||||
|
|
|
@ -30,6 +30,7 @@ final class ComposeViewModel {
|
||||||
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
|
||||||
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
|
||||||
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make intial event emit
|
let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void()) // use CurrentValueSubject to make intial event emit
|
||||||
|
let repliedToCellFrame = CurrentValueSubject<CGRect, Never>(.zero)
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
|
||||||
|
@ -56,6 +57,7 @@ final class ComposeViewModel {
|
||||||
let isMediaToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
let isMediaToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||||
let isPollToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
let isPollToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||||
let characterCount = CurrentValueSubject<Int, Never>(0)
|
let characterCount = CurrentValueSubject<Int, Never>(0)
|
||||||
|
let collectionViewState = CurrentValueSubject<CollectionViewState, Never>(.fold)
|
||||||
|
|
||||||
// for hashtag: #<hashag>' '
|
// for hashtag: #<hashag>' '
|
||||||
// for mention: @<mention>' '
|
// for mention: @<mention>' '
|
||||||
|
@ -377,6 +379,13 @@ final class ComposeViewModel {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ComposeViewModel {
|
||||||
|
enum CollectionViewState {
|
||||||
|
case fold // snap to input
|
||||||
|
case expand // snap to reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ComposeViewModel {
|
extension ComposeViewModel {
|
||||||
func createNewPollOptionIfPossible() {
|
func createNewPollOptionIfPossible() {
|
||||||
guard pollOptionAttributes.value.count < 4 else { return }
|
guard pollOptionAttributes.value.count < 4 else { return }
|
||||||
|
|
Loading…
Reference in New Issue