mirror of
https://github.com/mastodon/mastodon-ios
synced 2025-04-11 22:58:02 +02:00
feat: restore content warning input with black-yellow strip edges
This commit is contained in:
parent
b12825a96a
commit
3100c59a3b
@ -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",
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user