forked from zelo72/mastodon-ios
feat: add counter and emoji picker activity indicator
This commit is contained in:
parent
59889cd683
commit
87a6a4df77
|
@ -1171,8 +1171,8 @@
|
||||||
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
|
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
|
||||||
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
|
||||||
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
|
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
|
||||||
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */,
|
|
||||||
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */,
|
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */,
|
||||||
|
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */,
|
||||||
);
|
);
|
||||||
path = Compose;
|
path = Compose;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
|
@ -85,15 +85,8 @@ extension ComposeStatusSection {
|
||||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
|
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
|
||||||
cell.statusContentWarningEditorView.alpha = 1
|
cell.statusContentWarningEditorView.alpha = 1
|
||||||
} completion: { _ in
|
} completion: { _ in
|
||||||
if isContentWarningComposing {
|
|
||||||
cell.statusContentWarningEditorView.textView.becomeFirstResponder()
|
|
||||||
}
|
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
// restore responder if needs
|
|
||||||
if cell.statusContentWarningEditorView.textView.isFirstResponder {
|
|
||||||
cell.textEditorView.isEditing = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
cell.contentWarningContent
|
cell.contentWarningContent
|
||||||
|
|
|
@ -75,10 +75,10 @@ internal enum Asset {
|
||||||
internal static let buttonDefault = ColorAsset(name: "Colors/buttonDefault")
|
internal static let buttonDefault = ColorAsset(name: "Colors/buttonDefault")
|
||||||
internal static let buttonDisabled = ColorAsset(name: "Colors/buttonDisabled")
|
internal static let buttonDisabled = ColorAsset(name: "Colors/buttonDisabled")
|
||||||
internal static let buttonInactive = ColorAsset(name: "Colors/buttonInactive")
|
internal static let buttonInactive = ColorAsset(name: "Colors/buttonInactive")
|
||||||
|
internal static let danger = ColorAsset(name: "Colors/danger")
|
||||||
internal static let lightAlertYellow = ColorAsset(name: "Colors/lightAlertYellow")
|
internal static let lightAlertYellow = ColorAsset(name: "Colors/lightAlertYellow")
|
||||||
internal static let lightBackground = ColorAsset(name: "Colors/lightBackground")
|
internal static let lightBackground = ColorAsset(name: "Colors/lightBackground")
|
||||||
internal static let lightBrandBlue = ColorAsset(name: "Colors/lightBrandBlue")
|
internal static let lightBrandBlue = ColorAsset(name: "Colors/lightBrandBlue")
|
||||||
internal static let lightDangerRed = ColorAsset(name: "Colors/lightDangerRed")
|
|
||||||
internal static let lightDarkGray = ColorAsset(name: "Colors/lightDarkGray")
|
internal static let lightDarkGray = ColorAsset(name: "Colors/lightDarkGray")
|
||||||
internal static let lightDisabled = ColorAsset(name: "Colors/lightDisabled")
|
internal static let lightDisabled = ColorAsset(name: "Colors/lightDisabled")
|
||||||
internal static let lightInactive = ColorAsset(name: "Colors/lightInactive")
|
internal static let lightInactive = ColorAsset(name: "Colors/lightInactive")
|
||||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
protocol ComposeStatusPollOptionCollectionViewCellDelegate: class {
|
protocol ComposeStatusPollOptionCollectionViewCellDelegate: class {
|
||||||
|
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textFieldDidBeginEditing textField: UITextField)
|
||||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?)
|
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?)
|
||||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField)
|
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField)
|
||||||
}
|
}
|
||||||
|
@ -132,6 +133,12 @@ extension ComposeStatusPollOptionCollectionViewCell: DeleteBackwardResponseTextF
|
||||||
|
|
||||||
// MARK: - UITextFieldDelegate
|
// MARK: - UITextFieldDelegate
|
||||||
extension ComposeStatusPollOptionCollectionViewCell: UITextFieldDelegate {
|
extension ComposeStatusPollOptionCollectionViewCell: UITextFieldDelegate {
|
||||||
|
|
||||||
|
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
delegate?.composeStatusPollOptionCollectionViewCell(self, textFieldDidBeginEditing: textField)
|
||||||
|
}
|
||||||
|
|
||||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
if textField === pollOptionView.optionTextField {
|
if textField === pollOptionView.optionTextField {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
|
import MastodonSDK
|
||||||
import TwitterTextEditor
|
import TwitterTextEditor
|
||||||
|
|
||||||
final class ComposeViewController: UIViewController, NeedsDependency {
|
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
|
@ -102,14 +103,18 @@ final class ComposeViewController: UIViewController, NeedsDependency {
|
||||||
return documentPickerController
|
return documentPickerController
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeViewController {
|
extension ComposeViewController {
|
||||||
private static func createLayout() -> UICollectionViewLayout {
|
private static func createLayout() -> UICollectionViewLayout {
|
||||||
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(100))
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(100))
|
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
|
||||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
section.contentInsetsReference = .readableContent
|
section.contentInsetsReference = .readableContent
|
||||||
// section.interGroupSpacing = 10
|
// section.interGroupSpacing = 10
|
||||||
|
@ -232,22 +237,61 @@ extension ComposeViewController {
|
||||||
})
|
})
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind publish bar button state
|
||||||
viewModel.isPublishBarButtonItemEnabled
|
viewModel.isPublishBarButtonItemEnabled
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assign(to: \.isEnabled, on: publishBarButtonItem)
|
.assign(to: \.isEnabled, on: publishBarButtonItem)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind media button toolbar state
|
||||||
viewModel.isMediaToolbarButtonEnabled
|
viewModel.isMediaToolbarButtonEnabled
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assign(to: \.isEnabled, on: composeToolbarView.mediaButton)
|
.assign(to: \.isEnabled, on: composeToolbarView.mediaButton)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind poll button toolbar state
|
||||||
viewModel.isPollToolbarButtonEnabled
|
viewModel.isPollToolbarButtonEnabled
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assign(to: \.isEnabled, on: composeToolbarView.pollButton)
|
.assign(to: \.isEnabled, on: composeToolbarView.pollButton)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind custom emojis
|
// bind image picker toolbar state
|
||||||
|
viewModel.attachmentServices
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] attachmentServices in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.composeToolbarView.mediaButton.isEnabled = attachmentServices.count < 4
|
||||||
|
self.resetImagePicker()
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind visibility toolbar UI
|
||||||
|
viewModel.selectedStatusVisibility
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] type in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.composeToolbarView.visibilityButton.setImage(type.image, for: .normal)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
viewModel.characterCount
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] characterCount in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let count = ComposeViewModel.composeContentLimit - characterCount
|
||||||
|
self.composeToolbarView.characterCountLabel.text = "\(count)"
|
||||||
|
switch count {
|
||||||
|
case _ where count < 0:
|
||||||
|
self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 24, weight: .bold)
|
||||||
|
self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color
|
||||||
|
default:
|
||||||
|
self.composeToolbarView.characterCountLabel.font = .systemFont(ofSize: 15, weight: .regular)
|
||||||
|
self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind text editor for custom emojis update event
|
||||||
viewModel.customEmojiViewModel
|
viewModel.customEmojiViewModel
|
||||||
.compactMap { $0?.emojis }
|
.compactMap { $0?.emojis }
|
||||||
.switchToLatest()
|
.switchToLatest()
|
||||||
|
@ -261,22 +305,24 @@ extension ComposeViewController {
|
||||||
})
|
})
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
// bind image picker toolbar state
|
// bind custom emoji picker UI
|
||||||
viewModel.attachmentServices
|
viewModel.customEmojiViewModel
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] attachmentServices in
|
.map { viewModel -> AnyPublisher<[Mastodon.Entity.Emoji], Never> in
|
||||||
guard let self = self else { return }
|
guard let viewModel = viewModel else {
|
||||||
self.composeToolbarView.mediaButton.isEnabled = attachmentServices.count < 4
|
return Just([]).eraseToAnyPublisher()
|
||||||
self.resetImagePicker()
|
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
return viewModel.emojis.eraseToAnyPublisher()
|
||||||
|
|
||||||
viewModel.selectedStatusVisibility
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] type in
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.composeToolbarView.visibilityButton.setImage(type.image, for: .normal)
|
|
||||||
}
|
}
|
||||||
|
.switchToLatest()
|
||||||
|
.sink(receiveValue: { [weak self] emojis in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if emojis.isEmpty {
|
||||||
|
self.customEmojiPickerInputView.activityIndicatorView.startAnimating()
|
||||||
|
} else {
|
||||||
|
self.customEmojiPickerInputView.activityIndicatorView.stopAnimating()
|
||||||
|
}
|
||||||
|
})
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,6 +363,25 @@ extension ComposeViewController {
|
||||||
textEditorView()?.isEditing = true
|
textEditorView()?.isEditing = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func contentWarningEditorTextView() -> UITextView? {
|
||||||
|
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||||
|
let items = diffableDataSource.snapshot().itemIdentifiers
|
||||||
|
for item in items {
|
||||||
|
switch item {
|
||||||
|
case .input:
|
||||||
|
guard let indexPath = diffableDataSource.indexPath(for: item),
|
||||||
|
let cell = collectionView.cellForItem(at: indexPath) as? ComposeStatusContentCollectionViewCell else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return cell.statusContentWarningEditorView.textView
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
private func pollOptionCollectionViewCell(of item: ComposeStatusItem) -> ComposeStatusPollOptionCollectionViewCell? {
|
private func pollOptionCollectionViewCell(of item: ComposeStatusItem) -> ComposeStatusPollOptionCollectionViewCell? {
|
||||||
guard case .pollOption = item else { return nil }
|
guard case .pollOption = item else { return nil }
|
||||||
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
guard let diffableDataSource = viewModel.diffableDataSource else { return nil }
|
||||||
|
@ -587,6 +652,15 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||||
attributedString.addAttributes(attributes, range: match.range)
|
attributedString.addAttributes(attributes, range: match.range)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if string.count > ComposeViewModel.composeContentLimit {
|
||||||
|
var attributes = [NSAttributedString.Key: Any]()
|
||||||
|
attributes[.foregroundColor] = Asset.Colors.danger.color
|
||||||
|
let boundStart = string.index(string.startIndex, offsetBy: ComposeViewModel.composeContentLimit)
|
||||||
|
let boundEnd = string.endIndex
|
||||||
|
let range = boundStart..<boundEnd
|
||||||
|
attributedString.addAttributes(attributes, range: NSRange(range, in: string))
|
||||||
|
}
|
||||||
|
|
||||||
completion(attributedString)
|
completion(attributedString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -631,7 +705,20 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) {
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) {
|
||||||
|
// restore first responder for text editor when content warning dismiss
|
||||||
|
if viewModel.isContentWarningComposing.value {
|
||||||
|
if contentWarningEditorTextView()?.isFirstResponder == true {
|
||||||
|
markTextEditorViewBecomeFirstResponser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggle composing status
|
||||||
viewModel.isContentWarningComposing.value.toggle()
|
viewModel.isContentWarningComposing.value.toggle()
|
||||||
|
|
||||||
|
// active content warning after toggled
|
||||||
|
if viewModel.isContentWarningComposing.value {
|
||||||
|
contentWarningEditorTextView()?.becomeFirstResponder()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) {
|
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) {
|
||||||
|
@ -773,6 +860,14 @@ extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelega
|
||||||
// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate
|
// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate
|
||||||
extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate {
|
extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate {
|
||||||
|
|
||||||
|
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textFieldDidBeginEditing textField: UITextField) {
|
||||||
|
// FIXME: make poll section visible
|
||||||
|
// DispatchQueue.main.async {
|
||||||
|
// self.collectionView.scroll(to: .bottom, animated: true)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// handle delete backward event for poll option input
|
// handle delete backward event for poll option input
|
||||||
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) {
|
func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) {
|
||||||
guard (text ?? "").isEmpty else { return }
|
guard (text ?? "").isEmpty else { return }
|
||||||
|
|
|
@ -55,7 +55,6 @@ extension ComposeViewModel {
|
||||||
self.pollOptionAttributes.value = pollOptionAttributes
|
self.pollOptionAttributes.value = pollOptionAttributes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
self.diffableDataSource = diffableDataSource
|
self.diffableDataSource = diffableDataSource
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()
|
||||||
snapshot.appendSections([.repliedTo, .status, .attachment, .poll])
|
snapshot.appendSections([.repliedTo, .status, .attachment, .poll])
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
// Created by MainasuK Cirno on 2021-3-11.
|
// Created by MainasuK Cirno on 2021-3-11.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
|
@ -14,6 +15,8 @@ import MastodonSDK
|
||||||
|
|
||||||
final class ComposeViewModel {
|
final class ComposeViewModel {
|
||||||
|
|
||||||
|
static let composeContentLimit: Int = 500
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
// input
|
// input
|
||||||
|
@ -48,11 +51,13 @@ final class ComposeViewModel {
|
||||||
let isPublishBarButtonItemEnabled = CurrentValueSubject<Bool, Never>(false)
|
let isPublishBarButtonItemEnabled = CurrentValueSubject<Bool, Never>(false)
|
||||||
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)
|
||||||
|
|
||||||
// custom emojis
|
// custom emojis
|
||||||
var customEmojiViewModelSubscription: AnyCancellable?
|
var customEmojiViewModelSubscription: AnyCancellable?
|
||||||
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
|
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
|
||||||
let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
|
let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
|
||||||
|
let isLoadingCustomEmoji = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
// attachment
|
// attachment
|
||||||
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
|
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
|
||||||
|
@ -109,10 +114,30 @@ final class ComposeViewModel {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
// bind character count
|
||||||
|
Publishers.CombineLatest3(
|
||||||
|
composeStatusAttribute.composeContent.eraseToAnyPublisher(),
|
||||||
|
composeStatusAttribute.isContentWarningComposing.eraseToAnyPublisher(),
|
||||||
|
composeStatusAttribute.contentWarningContent.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.map { composeContent, isContentWarningComposing, contentWarningContent -> Int in
|
||||||
|
let composeContent = composeContent ?? ""
|
||||||
|
var count = composeContent.count
|
||||||
|
if isContentWarningComposing {
|
||||||
|
count += contentWarningContent.count
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
.assign(to: \.value, on: characterCount)
|
||||||
|
.store(in: &disposeBag)
|
||||||
// bind compose bar button item UI state
|
// bind compose bar button item UI state
|
||||||
let isComposeContentEmpty = composeStatusAttribute.composeContent
|
let isComposeContentEmpty = composeStatusAttribute.composeContent
|
||||||
.map { ($0 ?? "").isEmpty }
|
.map { ($0 ?? "").isEmpty }
|
||||||
let isComposeContentValid = Just(true).eraseToAnyPublisher()
|
let isComposeContentValid = composeStatusAttribute.composeContent
|
||||||
|
.map { composeContent -> Bool in
|
||||||
|
let composeContent = composeContent ?? ""
|
||||||
|
return composeContent.count <= ComposeViewModel.composeContentLimit
|
||||||
|
}
|
||||||
let isMediaEmpty = attachmentServices
|
let isMediaEmpty = attachmentServices
|
||||||
.map { $0.isEmpty }
|
.map { $0.isEmpty }
|
||||||
let isMediaUploadAllSuccess = attachmentServices
|
let isMediaUploadAllSuccess = attachmentServices
|
||||||
|
@ -278,6 +303,10 @@ final class ComposeViewModel {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ComposeViewModel {
|
extension ComposeViewModel {
|
||||||
|
@ -301,6 +330,6 @@ extension ComposeViewModel: MastodonAttachmentServiceDelegate {
|
||||||
extension ComposeViewModel: ComposePollAttributeDelegate {
|
extension ComposeViewModel: ComposePollAttributeDelegate {
|
||||||
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) {
|
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) {
|
||||||
// trigger update
|
// trigger update
|
||||||
pollOptionAttributes.value = pollOptionAttributes.value
|
// pollOptionAttributes.value = pollOptionAttributes.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,14 @@ final class ComposeToolbarView: UIView {
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let characterCountLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 15, weight: .regular)
|
||||||
|
label.text = "500"
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
|
@ -102,6 +110,16 @@ extension ComposeToolbarView {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
characterCountLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(characterCountLabel)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
characterCountLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
characterCountLabel.leadingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: 8),
|
||||||
|
characterCountLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
|
||||||
|
characterCountLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
characterCountLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
|
|
||||||
mediaButton.menu = createMediaContextMenu()
|
mediaButton.menu = createMediaContextMenu()
|
||||||
mediaButton.showsMenuAsPrimaryAction = true
|
mediaButton.showsMenuAsPrimaryAction = true
|
||||||
pollButton.addTarget(self, action: #selector(ComposeToolbarView.pollButtonDidPressed(_:)), for: .touchUpInside)
|
pollButton.addTarget(self, action: #selector(ComposeToolbarView.pollButtonDidPressed(_:)), for: .touchUpInside)
|
||||||
|
|
|
@ -17,6 +17,8 @@ final class CustomEmojiPickerInputView: UIInputView {
|
||||||
return collectionView
|
return collectionView
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
let activityIndicatorView = UIActivityIndicatorView(style: .large)
|
||||||
|
|
||||||
override init(frame: CGRect, inputViewStyle: UIInputView.Style) {
|
override init(frame: CGRect, inputViewStyle: UIInputView.Style) {
|
||||||
super.init(frame: frame, inputViewStyle: inputViewStyle)
|
super.init(frame: frame, inputViewStyle: inputViewStyle)
|
||||||
_init()
|
_init()
|
||||||
|
@ -33,6 +35,13 @@ extension CustomEmojiPickerInputView {
|
||||||
private func _init() {
|
private func _init() {
|
||||||
allowsSelfSizing = true
|
allowsSelfSizing = true
|
||||||
|
|
||||||
|
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(activityIndicatorView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(collectionView)
|
addSubview(collectionView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -41,6 +50,9 @@ extension CustomEmojiPickerInputView {
|
||||||
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
collectionView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
collectionView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
activityIndicatorView.hidesWhenStopped = true
|
||||||
|
activityIndicatorView.startAnimating()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ final class HomeTimelineNavigationBarView {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
static let offlineView: UIView = {
|
static let offlineView: UIView = {
|
||||||
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.lightDangerRed.color)
|
let view = HomeTimelineNavigationBarView.backgroundViewWithColor(color: Asset.Colors.danger.color)
|
||||||
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.offline)
|
let label = HomeTimelineNavigationBarView.contentLabel(text: L10n.Scene.HomeTimeline.NavigationBarState.offline)
|
||||||
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
|
HomeTimelineNavigationBarView.addLabelToView(label: label, view: view)
|
||||||
return view
|
return view
|
||||||
|
|
|
@ -106,7 +106,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
let usernameErrorPromptLabel: UILabel = {
|
let usernameErrorPromptLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
let color = Asset.Colors.lightDangerRed.color
|
let color = Asset.Colors.danger.color
|
||||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
@ -146,7 +146,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
let emailErrorPromptLabel: UILabel = {
|
let emailErrorPromptLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
let color = Asset.Colors.lightDangerRed.color
|
let color = Asset.Colors.danger.color
|
||||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
@ -177,7 +177,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
let passwordErrorPromptLabel: UILabel = {
|
let passwordErrorPromptLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
let color = Asset.Colors.lightDangerRed.color
|
let color = Asset.Colors.danger.color
|
||||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
@ -201,7 +201,7 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O
|
||||||
|
|
||||||
let reasonErrorPromptLabel: UILabel = {
|
let reasonErrorPromptLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
let color = Asset.Colors.lightDangerRed.color
|
let color = Asset.Colors.danger.color
|
||||||
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
let font = UIFont.preferredFont(forTextStyle: .caption1)
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -198,10 +198,10 @@ extension MastodonRegisterViewModel {
|
||||||
let attributeString = NSMutableAttributedString()
|
let attributeString = NSMutableAttributedString()
|
||||||
|
|
||||||
let image = MastodonRegisterViewModel.xmarkImage(font: font)
|
let image = MastodonRegisterViewModel.xmarkImage(font: font)
|
||||||
attributeString.append(attributedStringImage(with: image, tintColor: Asset.Colors.lightDangerRed.color))
|
attributeString.append(attributedStringImage(with: image, tintColor: Asset.Colors.danger.color))
|
||||||
attributeString.append(NSAttributedString(string: " "))
|
attributeString.append(NSAttributedString(string: " "))
|
||||||
|
|
||||||
let promptAttributedString = NSAttributedString(string: prompt, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: Asset.Colors.lightDangerRed.color])
|
let promptAttributedString = NSAttributedString(string: prompt, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: Asset.Colors.danger.color])
|
||||||
attributeString.append(promptAttributedString)
|
attributeString.append(promptAttributedString)
|
||||||
|
|
||||||
return attributeString
|
return attributeString
|
||||||
|
|
Loading…
Reference in New Issue