feat: restore emoji picker for post compose

This commit is contained in:
CMK 2022-11-13 19:42:50 +08:00
parent e7ef0f79c7
commit 88307057c0
18 changed files with 304 additions and 234 deletions

View File

@ -151,7 +151,6 @@
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */; };
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; };
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */; };
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */; };
DB22C92228E700A10082A9E9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB22C92128E700A10082A9E9 /* MastodonSDK */; };
DB22C92428E700A80082A9E9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB22C92328E700A80082A9E9 /* MastodonSDK */; };
DB22C92628E700AF0082A9E9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB22C92528E700AF0082A9E9 /* MastodonSDK */; };
@ -185,9 +184,6 @@
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; };
DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; };
DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB443CD32694627B00159B29 /* AppearanceView.swift */; };
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */; };
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */; };
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */; };
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B825EE289600BEFB67 /* UITableView.swift */; };
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */; };
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */; };
@ -680,7 +676,6 @@
DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+Diffable.swift"; sourceTree = "<group>"; };
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = "<group>"; };
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputViewModel.swift; sourceTree = "<group>"; };
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = "<group>"; };
@ -717,9 +712,6 @@
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = "<group>"; };
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DB443CD32694627B00159B29 /* AppearanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceView.swift; sourceTree = "<group>"; };
DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputView.swift; sourceTree = "<group>"; };
DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItemCollectionViewCell.swift; sourceTree = "<group>"; };
DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerHeaderCollectionReusableView.swift; sourceTree = "<group>"; };
DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = "<group>"; };
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = "<group>"; };
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = "<group>"; };
@ -1876,8 +1868,6 @@
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */,
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */,
DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */,
DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */,
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */,
);
path = View;
@ -2157,8 +2147,6 @@
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */,
DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */,
DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */,
);
path = CollectionViewCell;
sourceTree = "<group>";
@ -3335,7 +3323,6 @@
DB98EB6227B215EB0082E365 /* ReportResultViewController.swift in Sources */,
DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */,
DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */,
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */,
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+State.swift in Sources */,
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */,
5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */,
@ -3349,7 +3336,6 @@
DBFEEC9B279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift in Sources */,
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */,
DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */,
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
DB98EB5C27B10A730082E365 /* ReportSupplementaryViewModel.swift in Sources */,
DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */,
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
@ -3433,7 +3419,6 @@
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */,
2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */,
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */,
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */,
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
@ -3457,7 +3442,6 @@
DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */,
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */,
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */,
DB7A9F932818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift in Sources */,
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,

View File

@ -95,11 +95,7 @@ final class ComposeViewController: UIViewController, NeedsDependency {
// }
// }
//
// // CustomEmojiPickerView
// let customEmojiPickerInputView: CustomEmojiPickerInputView = {
// let view = CustomEmojiPickerInputView(frame: CGRect(x: 0, y: 0, width: 0, height: 300), inputViewStyle: .keyboard)
// return view
// }()
//
// let composeToolbarView = ComposeToolbarView()
// var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
@ -107,7 +103,6 @@ final class ComposeViewController: UIViewController, NeedsDependency {
//
//
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
@ -225,13 +220,6 @@ extension ComposeViewController {
// }
// .store(in: &disposeBag)
// customEmojiPickerInputView.collectionView.delegate = self
// viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView
// viewModel.setupCustomEmojiPickerDiffableDataSource(
// for: customEmojiPickerInputView.collectionView,
// dependency: self
// )
// viewModel.composeStatusContentTableViewCell.delegate = self
//
// // update layout when keyboard show/dismiss
@ -350,19 +338,6 @@ extension ComposeViewController {
// }
// .store(in: &disposeBag)
//
// // bind custom emoji picker UI
// viewModel.customEmojiViewModel?.emojis
// .receive(on: DispatchQueue.main)
// .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)
//
// configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar.value)
// Publishers.CombineLatest(
// keyboardHasShortcutBar,
@ -694,30 +669,6 @@ extension ComposeViewController {
//// MARK: - UITableViewDelegate
//extension ComposeViewController: UITableViewDelegate { }
//
//// MARK: - UICollectionViewDelegate
//extension ComposeViewController: UICollectionViewDelegate {
//
// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
//
// if collectionView === customEmojiPickerInputView.collectionView {
// guard let diffableDataSource = viewModel.customEmojiPickerDiffableDataSource else { return }
// let item = diffableDataSource.itemIdentifier(for: indexPath)
// guard case let .emoji(attribute) = item else { return }
// let emoji = attribute.emoji
//
// // make click sound
// UIDevice.current.playInputClick()
//
// // retrieve active text input and insert emoji
// // the trailing space is REQUIRED to make regex happy
// _ = viewModel.customEmojiPickerInputViewModel.insertText(":\(emoji.shortcode): ")
// } else {
// // do nothing
// }
// }
//}
// MARK: - UIAdaptivePresentationControllerDelegate
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
@ -877,49 +828,7 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
// return true
// }
//}
//
//// MARK: - AutoCompleteViewControllerDelegate
//extension ComposeViewController: AutoCompleteViewControllerDelegate {
// func autoCompleteViewController(_ viewController: AutoCompleteViewController, didSelectItem item: AutoCompleteItem) {
// guard let info = viewModel.autoCompleteInfo else { return }
// let _replacedText: String? = {
// var text: String
// switch item {
// case .hashtag(let hashtag):
// text = "#" + hashtag.name
// case .hashtagV1(let hashtagName):
// text = "#" + hashtagName
// case .account(let account):
// text = "@" + account.acct
// case .emoji(let emoji):
// text = ":" + emoji.shortcode + ":"
// case .bottomLoader:
// return nil
// }
// return text
// }()
// guard let replacedText = _replacedText else { return }
// guard let text = textEditorView.textView.text else { return }
//
// let range = NSRange(info.toHighlightEndRange, in: text)
// textEditorView.textStorage.replaceCharacters(in: range, with: replacedText)
// DispatchQueue.main.async {
// self.textEditorView.textView.insertText(" ") // trigger textView delegate update
// }
// viewModel.autoCompleteInfo = nil
//
// switch item {
// case .emoji, .bottomLoader:
// break
// default:
// // set selected range except emoji
// let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0)
// guard textEditorView.textStorage.length <= newRange.location else { return }
// textEditorView.textView.selectedRange = newRange
// }
// }
//}
//
//extension ComposeViewController {
// override var keyCommands: [UIKeyCommand]? {
// composeKeyCommands

View File

@ -51,43 +51,6 @@ extension ComposeViewModel {
// // setup data source
// tableView.dataSource = self
// }
//
// func setupCustomEmojiPickerDiffableDataSource(
// for collectionView: UICollectionView,
// dependency: NeedsDependency
// ) {
// let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource(
// for: collectionView,
// dependency: dependency
// )
// self.customEmojiPickerDiffableDataSource = diffableDataSource
//
// let _domain = customEmojiViewModel?.domain
// customEmojiViewModel?.emojis
// .receive(on: DispatchQueue.main)
// .sink { [weak self, weak diffableDataSource] emojis in
// guard let _ = self else { return }
// guard let diffableDataSource = diffableDataSource else { return }
//
// var snapshot = NSDiffableDataSourceSnapshot<CustomEmojiPickerSection, CustomEmojiPickerItem>()
// let domain = _domain?.uppercased() ?? " "
// let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain)
// snapshot.appendSections([customEmojiSection])
// let items: [CustomEmojiPickerItem] = {
// var items = [CustomEmojiPickerItem]()
// for emoji in emojis where emoji.visibleInPicker {
// let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji)
// let item = CustomEmojiPickerItem.emoji(attribute: attribute)
// items.append(item)
// }
// return items
// }()
// snapshot.appendItems(items, toSection: customEmojiSection)
//
// diffableDataSource.apply(snapshot)
// }
// .store(in: &disposeBag)
// }
}

View File

@ -102,11 +102,6 @@ final class ComposeViewModel: NSObject {
// // for mention: "@<mention> "
// var preInsertedContent: String?
//
// // custom emojis
// let customEmojiViewModel: EmojiService.CustomEmojiViewModel?
// let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
// @Published var isLoadingCustomEmoji = false
//
// // attachment
// @Published var attachmentServices: [MastodonAttachmentService] = []
//

View File

@ -8,28 +8,28 @@
import Foundation
import MastodonSDK
enum CustomEmojiPickerItem {
public enum CustomEmojiPickerItem {
case emoji(attribute: CustomEmojiAttribute)
}
extension CustomEmojiPickerItem: Equatable, Hashable { }
extension CustomEmojiPickerItem {
final class CustomEmojiAttribute: Equatable, Hashable {
let id = UUID()
public final class CustomEmojiAttribute: Equatable, Hashable {
public let id = UUID()
let emoji: Mastodon.Entity.Emoji
public let emoji: Mastodon.Entity.Emoji
init(emoji: Mastodon.Entity.Emoji) {
public init(emoji: Mastodon.Entity.Emoji) {
self.emoji = emoji
}
static func == (lhs: CustomEmojiPickerItem.CustomEmojiAttribute, rhs: CustomEmojiPickerItem.CustomEmojiAttribute) -> Bool {
public static func == (lhs: CustomEmojiPickerItem.CustomEmojiAttribute, rhs: CustomEmojiPickerItem.CustomEmojiAttribute) -> Bool {
return lhs.id == rhs.id &&
lhs.emoji.shortcode == rhs.emoji.shortcode
}
func hash(into hasher: inout Hasher) {
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

View File

@ -5,55 +5,55 @@
// Created by MainasuK on 22/10/10.
//
import Foundation
import UIKit
import MastodonCore
extension CustomEmojiPickerSection {
// static func collectionViewDiffableDataSource(
// collectionView: UICollectionView,
// dependency: NeedsDependency
// ) -> UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem> {
// let dataSource = UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>(collectionView: collectionView) { [weak dependency] collectionView, indexPath, item -> UICollectionViewCell? in
// guard let _ = dependency else { return nil }
// switch item {
// case .emoji(let attribute):
// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell
// let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill)
// .af.imageRounded(withCornerRadius: 4)
//
// let isAnimated = !UserDefaults.shared.preferredStaticEmoji
// let url = URL(string: isAnimated ? attribute.emoji.url : attribute.emoji.staticURL)
// cell.emojiImageView.sd_setImage(
// with: url,
// placeholderImage: placeholder,
// options: [],
// context: nil
// )
// cell.accessibilityLabel = attribute.emoji.shortcode
// return cell
// }
// }
//
// dataSource.supplementaryViewProvider = { [weak dataSource] collectionView, kind, indexPath -> UICollectionReusableView? in
// guard let dataSource = dataSource else { return nil }
// let sections = dataSource.snapshot().sectionIdentifiers
// guard indexPath.section < sections.count else { return nil }
// let section = sections[indexPath.section]
//
// switch kind {
// case String(describing: CustomEmojiPickerHeaderCollectionReusableView.self):
// let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView
// switch section {
// case .emoji(let name):
// header.titleLabel.text = name
// }
// return header
// default:
// assertionFailure()
// return nil
// }
// }
//
// return dataSource
// }
static func collectionViewDiffableDataSource(
collectionView: UICollectionView,
context: AppContext
) -> UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem> {
let dataSource = UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>(collectionView: collectionView) { [weak context] collectionView, indexPath, item -> UICollectionViewCell? in
guard let _ = context else { return nil }
switch item {
case .emoji(let attribute):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell
let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill)
.af.imageRounded(withCornerRadius: 4)
let isAnimated = !UserDefaults.shared.preferredStaticEmoji
let url = URL(string: isAnimated ? attribute.emoji.url : attribute.emoji.staticURL)
cell.emojiImageView.sd_setImage(
with: url,
placeholderImage: placeholder,
options: [],
context: nil
)
cell.accessibilityLabel = attribute.emoji.shortcode
return cell
}
}
dataSource.supplementaryViewProvider = { [weak dataSource] collectionView, kind, indexPath -> UICollectionReusableView? in
guard let dataSource = dataSource else { return nil }
let sections = dataSource.snapshot().sectionIdentifiers
guard indexPath.section < sections.count else { return nil }
let section = sections[indexPath.section]
switch kind {
case String(describing: CustomEmojiPickerHeaderCollectionReusableView.self):
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView
switch section {
case .emoji(let name):
header.titleLabel.text = name
}
return header
default:
assertionFailure()
return nil
}
}
return dataSource
}
}

View File

@ -102,7 +102,7 @@ extension AutoCompleteViewModel.State {
return
}
guard let customEmojiViewModel = viewModel.customEmojiViewModel.value else {
guard let customEmojiViewModel = viewModel.customEmojiViewModel else {
await enter(state: Fail.self)
return
}

View File

@ -20,7 +20,7 @@ final class AutoCompleteViewModel {
let authContext: AuthContext
public let inputText = CurrentValueSubject<String, Never>("") // contains "@" or "#" prefix
public let symbolBoundingRect = CurrentValueSubject<CGRect, Never>(.zero)
public let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
public let customEmojiViewModel: EmojiService.CustomEmojiViewModel?
// output
public var autoCompleteItems = CurrentValueSubject<[AutoCompleteItem], Never>([])
@ -40,6 +40,8 @@ final class AutoCompleteViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authContext.mastodonAuthenticationBox.domain)
// end init
autoCompleteItems
.receive(on: DispatchQueue.main)

View File

@ -72,6 +72,15 @@ public final class ComposeContentViewController: UIViewController {
documentPickerController.delegate = self
return documentPickerController
}()
// emoji picker inputView
let customEmojiPickerInputView: CustomEmojiPickerInputView = {
let view = CustomEmojiPickerInputView(
frame: CGRect(x: 0, y: 0, width: 0, height: 300),
inputViewStyle: .keyboard
)
return view
}()
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
@ -83,6 +92,8 @@ extension ComposeContentViewController {
public override func viewDidLoad() {
super.viewDidLoad()
viewModel.delegate = self
// setup view
self.setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
@ -106,6 +117,12 @@ extension ComposeContentViewController {
tableView.delegate = self
viewModel.setupDataSource(tableView: tableView)
// setup emoji picker
customEmojiPickerInputView.collectionView.delegate = self
viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView
viewModel.setupCustomEmojiPickerDiffableDataSource(collectionView: customEmojiPickerInputView.collectionView)
// setup toolbar
let toolbarHostingView = UIHostingController(rootView: composeContentToolbarView)
toolbarHostingView.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(toolbarHostingView.view)
@ -128,6 +145,7 @@ extension ComposeContentViewController {
view.bottomAnchor.constraint(equalTo: composeContentToolbarBackgroundView.bottomAnchor),
])
// bind keyboard
let keyboardHasShortcutBar = CurrentValueSubject<Bool, Never>(traitCollection.userInterfaceIdiom == .pad) // update default value later
let keyboardEventPublishers = Publishers.CombineLatest3(
KeyboardResponderService.shared.isShow,
@ -256,6 +274,19 @@ extension ComposeContentViewController {
}
.store(in: &disposeBag)
// bind emoji picker
viewModel.customEmojiViewModel?.emojis
.receive(on: DispatchQueue.main)
.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)
// bind toolbar
bindToolbarViewModel()
}
@ -485,5 +516,115 @@ extension ComposeContentViewController: AutoCompleteViewControllerDelegate {
didSelectItem item: AutoCompleteItem
) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did select item: \(String(describing: item))")
guard let info = viewModel.autoCompleteInfo else { return }
guard let metaText = viewModel.contentMetaText else { return }
let _replacedText: String? = {
var text: String
switch item {
case .hashtag(let hashtag):
text = "#" + hashtag.name
case .hashtagV1(let hashtagName):
text = "#" + hashtagName
case .account(let account):
text = "@" + account.acct
case .emoji(let emoji):
text = ":" + emoji.shortcode + ":"
case .bottomLoader:
return nil
}
return text
}()
guard let replacedText = _replacedText else { return }
guard let text = metaText.textView.text else { return }
let range = NSRange(info.toHighlightEndRange, in: text)
metaText.textStorage.replaceCharacters(in: range, with: replacedText)
viewModel.autoCompleteInfo = nil
// set selected range
let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0)
guard metaText.textStorage.length <= newRange.location else { return }
metaText.textView.selectedRange = newRange
// append a space and trigger textView delegate update
DispatchQueue.main.async {
metaText.textView.insertText(" ")
}
}
}
// MARK: - UICollectionViewDelegate
extension ComposeContentViewController: UICollectionViewDelegate {
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
switch collectionView {
case customEmojiPickerInputView.collectionView:
guard let diffableDataSource = viewModel.customEmojiPickerDiffableDataSource else { return }
let item = diffableDataSource.itemIdentifier(for: indexPath)
guard case let .emoji(attribute) = item else { return }
let emoji = attribute.emoji
// make click sound
UIDevice.current.playInputClick()
// retrieve active text input and insert emoji
// the trailing space is REQUIRED to make regex happy
_ = viewModel.customEmojiPickerInputViewModel.insertText(":\(emoji.shortcode): ")
default:
assertionFailure()
}
} // end func
}
// MARK: - ComposeContentViewModelDelegate
extension ComposeContentViewController: ComposeContentViewModelDelegate {
public func composeContentViewModel(
_ viewModel: ComposeContentViewModel,
handleAutoComplete info: ComposeContentViewModel.AutoCompleteInfo
) -> Bool {
let snapshot = autoCompleteViewController.viewModel.diffableDataSource.snapshot()
guard let item = snapshot.itemIdentifiers.first else { return false }
// FIXME: redundant code
guard let metaText = viewModel.contentMetaText else { return false }
guard let text = metaText.textView.text else { return false }
let _replacedText: String? = {
var text: String
switch item {
case .hashtag(let hashtag):
text = "#" + hashtag.name
case .hashtagV1(let hashtagName):
text = "#" + hashtagName
case .account(let account):
text = "@" + account.acct
case .emoji(let emoji):
text = ":" + emoji.shortcode + ":"
case .bottomLoader:
return nil
}
return text
}()
guard let replacedText = _replacedText else { return false }
let range = NSRange(info.toHighlightEndRange, in: text)
metaText.textStorage.replaceCharacters(in: range, with: replacedText)
viewModel.autoCompleteInfo = nil
// set selected range
let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0)
guard metaText.textStorage.length <= newRange.location else { return true }
metaText.textView.selectedRange = newRange
// append a space and trigger textView delegate update
DispatchQueue.main.async {
metaText.textView.insertText(" ")
}
return true
}
}

View File

@ -74,6 +74,7 @@ extension ComposeContentViewModel {
}
}
// MARK: - UITableViewDataSource
extension ComposeContentViewModel: UITableViewDataSource {
public func numberOfSections(in tableView: UITableView) -> Int {
return Section.allCases.count
@ -99,3 +100,42 @@ extension ComposeContentViewModel: UITableViewDataSource {
}
}
}
extension ComposeContentViewModel {
func setupCustomEmojiPickerDiffableDataSource(
collectionView: UICollectionView
) {
let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource(
collectionView: collectionView,
context: context
)
self.customEmojiPickerDiffableDataSource = diffableDataSource
let domain = authContext.mastodonAuthenticationBox.domain.uppercased()
customEmojiViewModel?.emojis
.receive(on: DispatchQueue.main)
.sink { [weak self, weak diffableDataSource] emojis in
guard let _ = self else { return }
guard let diffableDataSource = diffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<CustomEmojiPickerSection, CustomEmojiPickerItem>()
let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain)
snapshot.appendSections([customEmojiSection])
let items: [CustomEmojiPickerItem] = {
var items = [CustomEmojiPickerItem]()
for emoji in emojis where emoji.visibleInPicker {
let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji)
let item = CustomEmojiPickerItem.emoji(attribute: attribute)
items.append(item)
}
return items
}()
snapshot.appendItems(items, toSection: customEmojiSection)
diffableDataSource.apply(snapshot)
}
.store(in: &disposeBag)
}
}

View File

@ -37,7 +37,7 @@ extension ComposeContentViewModel: MetaTextDelegate {
let content = MastodonContent(
content: textInput,
emojis: [:] // TODO: emojiViewModel?.emojis.asDictionary ?? [:]
emojis: [:] // customEmojiViewModel?.emojis.value.asDictionary ?? [:]
)
let metaContent = MastodonMetaContent.convert(text: content)
return metaContent
@ -48,7 +48,7 @@ extension ComposeContentViewModel: MetaTextDelegate {
let content = MastodonContent(
content: textInput,
emojis: [:] // emojiViewModel?.emojis.asDictionary ?? [:]
emojis: [:] // customEmojiViewModel?.emojis.value.asDictionary ?? [:]
)
let metaContent = MastodonMetaContent.convert(text: content)
return metaContent

View File

@ -64,6 +64,12 @@ extension ComposeContentViewModel: UITextViewDelegate {
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
switch textView {
case contentMetaText?.textView:
if text == " ", let autoCompleteInfo = self.autoCompleteInfo {
assert(delegate != nil)
let isHandled = delegate?.composeContentViewModel(self, handleAutoComplete: autoCompleteInfo) ?? false
return !isHandled
}
return true
case contentWarningMetaText?.textView:
let isReturn = text == "\n"

View File

@ -15,6 +15,10 @@ import MastodonMeta
import MastodonCore
import MastodonSDK
public protocol ComposeContentViewModelDelegate: AnyObject {
func composeContentViewModel(_ viewModel: ComposeContentViewModel, handleAutoComplete info: ComposeContentViewModel.AutoCompleteInfo) -> Bool
}
public final class ComposeContentViewModel: NSObject, ObservableObject {
let logger = Logger(subsystem: "ComposeContentViewModel", category: "ViewModel")
@ -28,6 +32,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// input
let context: AppContext
let kind: Kind
weak var delegate: ComposeContentViewModelDelegate?
@Published var viewLayoutFrame = ViewLayoutFrame()
@ -38,6 +43,9 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
@Published var autoCompleteRetryLayoutTimes = 0
@Published var autoCompleteInfo: AutoCompleteInfo? = nil
// emoji
var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>?
// output
// limit
@ -46,8 +54,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// content
public weak var contentMetaText: MetaText? {
didSet {
// guard let textView = contentMetaText?.textView else { return }
// customEmojiPickerInputViewModel.configure(textInput: textView)
guard let textView = contentMetaText?.textView else { return }
customEmojiPickerInputViewModel.configure(textInput: textView)
}
}
@Published public var initialContent = ""
@ -60,8 +68,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// content warning
weak var contentWarningMetaText: MetaText? {
didSet {
//guard let textView = contentWarningMetaText?.textView else { return }
//customEmojiPickerInputViewModel.configure(textInput: textView)
guard let textView = contentWarningMetaText?.textView else { return }
customEmojiPickerInputViewModel.configure(textInput: textView)
}
}
@Published public var isContentWarningActive = false
@ -95,6 +103,9 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// emoji
@Published var isEmojiActive = false
let customEmojiViewModel: EmojiService.CustomEmojiViewModel?
let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
@Published var isLoadingCustomEmoji = false
// visibility
@Published var visibility: Mastodon.Entity.Status.Visibility
@ -148,6 +159,9 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
}
return visibility
}()
self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(
for: authContext.mastodonAuthenticationBox.domain
)
super.init()
// end init
@ -192,6 +206,10 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
}
}
.store(in: &disposeBag)
// bind emoji inputView
$isEmojiActive.assign(to: &customEmojiPickerInputViewModel.$isCustomEmojiComposing)
}
deinit {
@ -215,7 +233,7 @@ extension ComposeContentViewModel {
}
extension ComposeContentViewModel {
struct AutoCompleteInfo {
public struct AutoCompleteInfo {
// model
let inputText: Substring
// range

View File

@ -9,7 +9,6 @@ import UIKit
import Combine
import MetaTextKit
import MastodonCore
import MastodonUI
final class CustomEmojiPickerInputViewModel {
@ -20,8 +19,7 @@ final class CustomEmojiPickerInputViewModel {
// input
weak var customEmojiPickerInputView: CustomEmojiPickerInputView?
// output
let isCustomEmojiComposing = CurrentValueSubject<Bool, Never>(false)
@Published var isCustomEmojiComposing = false
}
@ -51,27 +49,28 @@ extension CustomEmojiPickerInputViewModel {
for reference in customEmojiReplaceableTextInputReferences {
guard let textInput = reference.value else { continue }
guard textInput.isFirstResponder == true else { continue }
guard let selectedTextRange = textInput.selectedTextRange else { continue }
// guard let selectedTextRange = textInput.selectedTextRange else { continue }
textInput.insertText(text)
// FIXME: inline emoji
// due to insert text render as attachment
// the cursor reset logic not works
// hack with hard code +2 offset
assert(text.hasSuffix(": "))
guard text.hasPrefix(":") && text.hasSuffix(": ") else { continue }
if let _ = textInput as? MetaTextView {
if let newPosition = textInput.position(from: selectedTextRange.start, offset: 2) {
let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition)
textInput.selectedTextRange = newSelectedTextRange
}
} else {
if let newPosition = textInput.position(from: selectedTextRange.start, offset: text.length) {
let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition)
textInput.selectedTextRange = newSelectedTextRange
}
}
// assert(text.hasSuffix(": "))
// guard text.hasPrefix(":") && text.hasSuffix(": ") else { continue }
//
// if let _ = textInput as? MetaTextView {
// if let newPosition = textInput.position(from: selectedTextRange.start, offset: 2) {
// let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition)
// textInput.selectedTextRange = newSelectedTextRange
// }
// } else {
// if let newPosition = textInput.position(from: selectedTextRange.start, offset: text.length) {
// let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition)
// textInput.selectedTextRange = newSelectedTextRange
// }
// }
return reference
}
@ -81,3 +80,16 @@ extension CustomEmojiPickerInputViewModel {
}
extension CustomEmojiPickerInputViewModel {
public func configure(textInput: CustomEmojiReplaceableTextInput) {
$isCustomEmojiComposing
.receive(on: DispatchQueue.main)
.sink { [weak self] isCustomEmojiComposing in
guard let self = self else { return }
textInput.inputView = isCustomEmojiComposing ? self.customEmojiPickerInputView : nil
textInput.reloadInputViews()
self.append(customEmojiReplaceableTextInput: textInput)
}
.store(in: &disposeBag)
}
}

View File

@ -185,7 +185,7 @@ extension ComposeContentView {
index: _index,
deleteBackwardResponseTextFieldRelayDelegate: viewModel
) { textField in
// viewModel.customEmojiPickerInputViewModel.configure(textInput: textField)
viewModel.customEmojiPickerInputViewModel.configure(textInput: textField)
}
}
if viewModel.maxPollOptionLimit != viewModel.pollOptions.count {