feat: restore content warning input with black-yellow strip edges

This commit is contained in:
CMK 2022-10-28 19:06:18 +08:00
parent b12825a96a
commit 3100c59a3b
8 changed files with 284 additions and 24 deletions

View File

@ -126,6 +126,15 @@
"version" : "5.12.5"
}
},
{
"identity" : "stripes",
"kind" : "remoteSourceControl",
"location" : "https://github.com/eneko/Stripes.git",
"state" : {
"revision" : "d533fd44b8043a3abbf523e733599173d6f98c11",
"version" : "0.2.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",

View File

@ -48,6 +48,7 @@ let package = Package(
.package(url: "https://github.com/vtourraine/ThirdPartyMailer.git", from: "2.1.0"),
.package(url: "https://github.com/woxtu/UIHostingConfigurationBackport.git", from: "0.1.0"),
.package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.12.0"),
.package(url: "https://github.com/eneko/Stripes.git", from: "0.2.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@ -122,6 +123,7 @@ let package = Package(
.product(name: "MetaTextKit", package: "MetaTextKit"),
.product(name: "CropViewController", package: "TOCropViewController"),
.product(name: "PanModal", package: "PanModal"),
.product(name: "Stripes", package: "Stripes"),
]
),
.testTarget(

View File

@ -260,6 +260,9 @@ extension ComposeContentViewController {
viewModel.$isPollActive.assign(to: &composeContentToolbarViewModel.$isPollActive)
viewModel.$isEmojiActive.assign(to: &composeContentToolbarViewModel.$isEmojiActive)
viewModel.$isContentWarningActive.assign(to: &composeContentToolbarViewModel.$isContentWarningActive)
viewModel.$maxTextInputLimit.assign(to: &composeContentToolbarViewModel.$maxTextInputLimit)
viewModel.$contentWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWeightedLength)
viewModel.$contentWarningWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWarningWeightedLength)
}
}
@ -385,6 +388,16 @@ extension ComposeContentViewController: ComposeContentToolbarViewDelegate {
self.viewModel.isEmojiActive.toggle()
case .contentWarning:
self.viewModel.isContentWarningActive.toggle()
if self.viewModel.isContentWarningActive {
Task { @MainActor in
try? await Task.sleep(nanoseconds: .second / 20) // 0.05s
self.viewModel.setContentWarningTextViewFirstResponderIfNeeds()
} // end Task
} else {
if self.viewModel.contentWarningMetaText?.textView.isFirstResponder == true {
self.viewModel.setContentTextViewFirstResponderIfNeeds()
}
}
case .visibility:
assertionFailure()
}

View File

@ -0,0 +1,57 @@
//
// ComposeContentViewModel+MetaTextDelegate.swift
//
//
// Created by MainasuK on 2022/10/28.
//
import os.log
import UIKit
import MetaTextKit
import TwitterMeta
import MastodonMeta
// MARK: - MetaTextDelegate
extension ComposeContentViewModel: MetaTextDelegate {
public enum MetaTextViewKind: Int {
case none
case content
case contentWarning
}
public func metaText(
_ metaText: MetaText,
processEditing textStorage: MetaTextStorage
) -> MetaContent? {
let kind = MetaTextViewKind(rawValue: metaText.textView.tag) ?? .none
switch kind {
case .none:
assertionFailure()
return nil
case .content:
let textInput = textStorage.string
self.content = textInput
let content = MastodonContent(
content: textInput,
emojis: [:] // TODO: emojiViewModel?.emojis.asDictionary ?? [:]
)
let metaContent = MastodonMetaContent.convert(text: content)
return metaContent
case .contentWarning:
let textInput = textStorage.string.replacingOccurrences(of: "\n", with: " ")
self.contentWarning = textInput
let content = MastodonContent(
content: textInput,
emojis: [:] // emojiViewModel?.emojis.asDictionary ?? [:]
)
let metaContent = MastodonMetaContent.convert(text: content)
return metaContent
}
}
}

View File

@ -6,12 +6,13 @@
//
import os.log
import Foundation
import UIKit
import Combine
import CoreDataStack
import MastodonCore
import Meta
import MastodonMeta
import MetaTextKit
public final class ComposeContentViewModel: NSObject, ObservableObject {
@ -30,6 +31,42 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
@Published var viewLayoutFrame = ViewLayoutFrame()
@Published var authContext: AuthContext
// output
// limit
@Published public var maxTextInputLimit = 500
// content
public weak var contentMetaText: MetaText? {
didSet {
// guard let textView = contentMetaText?.textView else { return }
// customEmojiPickerInputViewModel.configure(textInput: textView)
}
}
@Published public var initialContent = ""
@Published public var content = ""
@Published public var contentWeightedLength = 0
@Published public var isContentEmpty = true
@Published public var isContentValid = true
@Published public var isContentEditing = false
// content warning
weak var contentWarningMetaText: MetaText? {
didSet {
//guard let textView = contentWarningMetaText?.textView else { return }
//customEmojiPickerInputViewModel.configure(textInput: textView)
}
}
@Published public var isContentWarningActive = false
@Published public var contentWarning = ""
@Published public var contentWarningWeightedLength = 0 // set 0 when not composing
@Published public var isContentWarningEditing = false
// author
@Published var avatarURL: URL?
@Published var name: MetaContent = PlaintextMetaContent(string: "")
@Published var username: String = ""
// poll
@Published var isPollActive = false
@Published public var pollOptions: [PollComposeItem.Option] = {
@ -42,23 +79,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
@Published public var pollExpireConfigurationOption: PollComposeItem.ExpireConfiguration.Option = .oneDay
@Published public var maxPollOptionLimit = 4
// emoji
@Published var isEmojiActive = false
@Published var isContentWarningActive = false
// output
// content
@Published public var initialContent = ""
@Published public var content = ""
@Published public var contentWeightedLength = 0
@Published public var isContentEmpty = true
@Published public var isContentValid = true
@Published public var isContentEditing = false
// author
@Published var avatarURL: URL?
@Published var name: MetaContent = PlaintextMetaContent(string: "")
@Published var username: String = ""
// UI & UX
@Published var replyToCellFrame: CGRect = .zero
@ -87,6 +109,26 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
self.username = user.acctWithDomain
}
.store(in: &disposeBag)
// bind text
$content
.map { $0.count }
.assign(to: &$contentWeightedLength)
Publishers.CombineLatest(
$contentWarning,
$isContentWarningActive
)
.map { $1 ? $0.count : 0 }
.assign(to: &$contentWarningWeightedLength)
Publishers.CombineLatest3(
$contentWeightedLength,
$contentWarningWeightedLength,
$maxTextInputLimit
)
.map { $0 + $1 <= $2 }
.assign(to: &$isContentValid)
}
deinit {
@ -120,6 +162,72 @@ extension ComposeContentViewModel {
}
}
// MARK: - UITextViewDelegate
extension ComposeContentViewModel: UITextViewDelegate {
public func textViewDidBeginEditing(_ textView: UITextView) {
switch textView {
case contentMetaText?.textView:
isContentEditing = true
case contentWarningMetaText?.textView:
isContentWarningEditing = true
default:
break
}
}
public func textViewDidEndEditing(_ textView: UITextView) {
switch textView {
case contentMetaText?.textView:
isContentEditing = false
case contentWarningMetaText?.textView:
isContentWarningEditing = false
default:
break
}
}
public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
switch textView {
case contentMetaText?.textView:
return true
case contentWarningMetaText?.textView:
let isReturn = text == "\n"
if isReturn {
setContentTextViewFirstResponderIfNeeds()
}
return !isReturn
default:
assertionFailure()
return true
}
}
func insertContentText(text: String) {
guard let contentMetaText = self.contentMetaText else { return }
// FIXME: smart prefix and suffix
let string = contentMetaText.textStorage.string
let isEmpty = string.isEmpty
let hasPrefix = string.hasPrefix(" ")
if hasPrefix || isEmpty {
contentMetaText.textView.insertText(text)
} else {
contentMetaText.textView.insertText(" " + text)
}
}
func setContentTextViewFirstResponderIfNeeds() {
guard let contentMetaText = self.contentMetaText else { return }
guard !contentMetaText.textView.isFirstResponder else { return }
contentMetaText.textView.becomeFirstResponder()
}
func setContentWarningTextViewFirstResponderIfNeeds() {
guard let contentWarningMetaText = self.contentWarningMetaText else { return }
guard !contentWarningMetaText.textView.isFirstResponder else { return }
contentWarningMetaText.textView.becomeFirstResponder()
}
}
// MARK: - DeleteBackwardResponseTextFieldRelayDelegate
extension ComposeContentViewModel: DeleteBackwardResponseTextFieldRelayDelegate {

View File

@ -27,6 +27,10 @@ extension ComposeContentToolbarView {
@Published var isEmojiActive = false
@Published var isContentWarningActive = false
@Published public var maxTextInputLimit = 500
@Published public var contentWeightedLength = 0
@Published public var contentWarningWeightedLength = 0
// output
init(delegate: ComposeContentToolbarViewDelegate) {

View File

@ -74,8 +74,18 @@ struct ComposeContentToolbarView: View {
}
}
Spacer()
Text("Hello")
.font(.system(size: 16, weight: .regular))
let count: Int = {
if viewModel.isContentWarningActive {
return viewModel.contentWeightedLength + viewModel.contentWarningWeightedLength
} else {
return viewModel.contentWeightedLength
}
}()
let remains = viewModel.maxTextInputLimit - count
let isOverflow = remains < 0
Text("\(remains)")
.foregroundColor(Color(isOverflow ? UIColor.systemRed : UIColor.secondaryLabel))
.font(.system(size: isOverflow ? 18 : 16, weight: isOverflow ? .medium : .regular))
}
.padding(.leading, 4) // 4 + 12 = 16
.padding(.trailing, 16)

View File

@ -7,8 +7,10 @@
import os.log
import SwiftUI
import MastodonAsset
import MastodonCore
import MastodonLocalization
import Stripes
public struct ComposeContentView: View {
@ -22,14 +24,69 @@ public struct ComposeContentView: View {
public var body: some View {
VStack(spacing: .zero) {
Group {
// content warning
if viewModel.isContentWarningActive {
MetaTextViewRepresentable(
string: $viewModel.contentWarning,
width: viewModel.viewLayoutFrame.layoutFrame.width - ComposeContentView.margin * 2,
configurationHandler: { metaText in
viewModel.contentWarningMetaText = metaText
metaText.textView.attributedPlaceholder = {
var attributes = metaText.textAttributes
attributes[.foregroundColor] = UIColor.secondaryLabel
return NSAttributedString(
string: L10n.Scene.Compose.contentInputPlaceholder,
attributes: attributes
)
}()
metaText.textView.returnKeyType = .next
metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.contentWarning.rawValue
metaText.textView.delegate = viewModel
metaText.delegate = viewModel
}
)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, ComposeContentView.margin)
.background(
Color(UIColor.systemBackground)
.overlay(
HStack {
Stripes(config: StripesConfig(
background: Color.yellow,
foreground: Color.black,
degrees: 45,
barWidth: 2.5,
barSpacing: 3.5
))
.frame(width: ComposeContentView.margin * 0.5)
.frame(maxHeight: .infinity)
.id(UUID())
Spacer()
Stripes(config: StripesConfig(
background: Color.yellow,
foreground: Color.black,
degrees: 45,
barWidth: 2.5,
barSpacing: 3.5
))
.frame(width: ComposeContentView.margin * 0.5)
.frame(maxHeight: .infinity)
.scaleEffect(x: -1, y: 1, anchor: .center)
.id(UUID())
}
)
)
} // end if viewModel.isContentWarningActive
// author
authorView
.padding(.top, 14)
.padding(.horizontal, ComposeContentView.margin)
// content editor
MetaTextViewRepresentable(
string: $viewModel.content,
width: viewModel.viewLayoutFrame.layoutFrame.width - ComposeContentView.margin * 2,
configurationHandler: { metaText in
viewModel.contentMetaText = metaText
metaText.textView.attributedPlaceholder = {
var attributes = metaText.textAttributes
attributes[.foregroundColor] = UIColor.secondaryLabel
@ -39,16 +96,18 @@ public struct ComposeContentView: View {
)
}()
metaText.textView.keyboardType = .twitter
// metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.content.rawValue
// metaText.textView.delegate = viewModel
// metaText.delegate = viewModel
metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.content.rawValue
metaText.textView.delegate = viewModel
metaText.delegate = viewModel
metaText.textView.becomeFirstResponder()
}
)
.frame(minHeight: 100)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, ComposeContentView.margin)
// poll
pollView
.padding(.horizontal, ComposeContentView.margin)
}
.background(
GeometryReader { proxy in
@ -65,8 +124,6 @@ public struct ComposeContentView: View {
)
Spacer()
} // end VStack
.padding(.horizontal, ComposeContentView.margin)
// .frame(alignment: .top)
} // end body
}