feat: Implement status translation info footer and reversion
This commit is contained in:
parent
ac76e7f435
commit
1020ca531a
|
@ -88,7 +88,7 @@ extension StatusTableViewCell {
|
||||||
.store(in: &_disposeBag)
|
.store(in: &_disposeBag)
|
||||||
|
|
||||||
statusView.viewModel
|
statusView.viewModel
|
||||||
.$isTranslated
|
.$translatedFromLanguage
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(receiveValue: { [weak self] _ in
|
.sink(receiveValue: { [weak self] _ in
|
||||||
self?.invalidateIntrinsicContentSize()
|
self?.invalidateIntrinsicContentSize()
|
||||||
|
|
|
@ -83,7 +83,7 @@ extension StatusThreadRootTableViewCell {
|
||||||
statusView.contentMetaText.textView.isSelectable = true
|
statusView.contentMetaText.textView.isSelectable = true
|
||||||
|
|
||||||
statusView.viewModel
|
statusView.viewModel
|
||||||
.$isTranslated
|
.$translatedFromLanguage
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink(receiveValue: { [weak self] _ in
|
.sink(receiveValue: { [weak self] _ in
|
||||||
self?.invalidateIntrinsicContentSize()
|
self?.invalidateIntrinsicContentSize()
|
||||||
|
|
|
@ -55,18 +55,13 @@ extension StatusView {
|
||||||
configurePoll(status: status)
|
configurePoll(status: status)
|
||||||
configureToolbar(status: status)
|
configureToolbar(status: status)
|
||||||
configureFilter(status: status)
|
configureFilter(status: status)
|
||||||
|
viewModel.originalStatus = status
|
||||||
status.$translatedContent
|
[
|
||||||
.receive(on: DispatchQueue.main)
|
status.$translatedContent,
|
||||||
.compactMap { $0 }
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
self?.configureTranslated(status: status)
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
status.reblog?.$translatedContent
|
status.reblog?.$translatedContent
|
||||||
|
].compactMap { $0 }
|
||||||
|
.last?
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.compactMap { $0 }
|
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.configureTranslated(status: status)
|
self?.configureTranslated(status: status)
|
||||||
}
|
}
|
||||||
|
@ -247,6 +242,14 @@ extension StatusView {
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func revertTranslation() {
|
||||||
|
guard let originalStatus = viewModel.originalStatus else { return }
|
||||||
|
viewModel.translatedFromLanguage = nil
|
||||||
|
originalStatus.reblog?.translatedContent = nil
|
||||||
|
originalStatus.translatedContent = nil
|
||||||
|
configure(status: originalStatus)
|
||||||
|
}
|
||||||
|
|
||||||
func configureTranslated(status: Status) {
|
func configureTranslated(status: Status) {
|
||||||
let translatedContent: String? = {
|
let translatedContent: String? = {
|
||||||
if let translatedContent = status.reblog?.translatedContent {
|
if let translatedContent = status.reblog?.translatedContent {
|
||||||
|
@ -267,7 +270,7 @@ extension StatusView {
|
||||||
let content = MastodonContent(content: translatedContent, emojis: status.emojis.asDictionary)
|
let content = MastodonContent(content: translatedContent, emojis: status.emojis.asDictionary)
|
||||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||||
viewModel.content = metaContent
|
viewModel.content = metaContent
|
||||||
viewModel.isTranslated = true
|
viewModel.translatedFromLanguage = status.reblog?.language ?? status.language
|
||||||
} catch {
|
} catch {
|
||||||
assertionFailure(error.localizedDescription)
|
assertionFailure(error.localizedDescription)
|
||||||
viewModel.content = PlaintextMetaContent(string: "")
|
viewModel.content = PlaintextMetaContent(string: "")
|
||||||
|
@ -301,7 +304,7 @@ extension StatusView {
|
||||||
let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary)
|
let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary)
|
||||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||||
viewModel.content = metaContent
|
viewModel.content = metaContent
|
||||||
viewModel.isTranslated = false
|
viewModel.translatedFromLanguage = nil
|
||||||
} catch {
|
} catch {
|
||||||
assertionFailure(error.localizedDescription)
|
assertionFailure(error.localizedDescription)
|
||||||
viewModel.content = PlaintextMetaContent(string: "")
|
viewModel.content = PlaintextMetaContent(string: "")
|
||||||
|
|
|
@ -44,7 +44,7 @@ extension StatusView {
|
||||||
@Published public var isMyself = false
|
@Published public var isMyself = false
|
||||||
@Published public var isMuting = false
|
@Published public var isMuting = false
|
||||||
@Published public var isBlocking = false
|
@Published public var isBlocking = false
|
||||||
@Published public var isTranslated = false
|
@Published public var translatedFromLanguage: String?
|
||||||
|
|
||||||
@Published public var timestamp: Date?
|
@Published public var timestamp: Date?
|
||||||
public var timestampFormatter: ((_ date: Date) -> String)?
|
public var timestampFormatter: ((_ date: Date) -> String)?
|
||||||
|
@ -137,7 +137,7 @@ extension StatusView {
|
||||||
isContentSensitive = false
|
isContentSensitive = false
|
||||||
isMediaSensitive = false
|
isMediaSensitive = false
|
||||||
isSensitiveToggled = false
|
isSensitiveToggled = false
|
||||||
isTranslated = false
|
translatedFromLanguage = nil
|
||||||
|
|
||||||
activeFilters = []
|
activeFilters = []
|
||||||
filterContext = nil
|
filterContext = nil
|
||||||
|
@ -586,7 +586,7 @@ extension StatusView.ViewModel {
|
||||||
$isBookmark
|
$isBookmark
|
||||||
)
|
)
|
||||||
let publishersThree = Publishers.CombineLatest(
|
let publishersThree = Publishers.CombineLatest(
|
||||||
$isTranslated,
|
$translatedFromLanguage,
|
||||||
$language
|
$language
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -598,7 +598,7 @@ extension StatusView.ViewModel {
|
||||||
.sink { tupleOne, tupleTwo, tupleThree in
|
.sink { tupleOne, tupleTwo, tupleThree in
|
||||||
let (authorName, isMyself) = tupleOne
|
let (authorName, isMyself) = tupleOne
|
||||||
let (isMuting, isBlocking, isBookmark) = tupleTwo
|
let (isMuting, isBlocking, isBookmark) = tupleTwo
|
||||||
let (isTranslated, language) = tupleThree
|
let (translatedFromLanguage, language) = tupleThree
|
||||||
|
|
||||||
guard let name = authorName?.string else {
|
guard let name = authorName?.string else {
|
||||||
statusView.authorView.menuButton.menu = nil
|
statusView.authorView.menuButton.menu = nil
|
||||||
|
@ -611,7 +611,7 @@ extension StatusView.ViewModel {
|
||||||
isBlocking: isBlocking,
|
isBlocking: isBlocking,
|
||||||
isMyself: isMyself,
|
isMyself: isMyself,
|
||||||
isBookmarking: isBookmark,
|
isBookmarking: isBookmark,
|
||||||
isTranslated: isTranslated,
|
isTranslated: translatedFromLanguage != nil,
|
||||||
statusLanguage: language
|
statusLanguage: language
|
||||||
)
|
)
|
||||||
let (menu, actions) = authorView.setupAuthorMenu(menuContext: menuContext)
|
let (menu, actions) = authorView.setupAuthorMenu(menuContext: menuContext)
|
||||||
|
|
|
@ -176,6 +176,37 @@ public final class StatusView: UIView {
|
||||||
indicatorView.stopAnimating()
|
indicatorView.stopAnimating()
|
||||||
return indicatorView
|
return indicatorView
|
||||||
}()
|
}()
|
||||||
|
private let translatedInfoLabel = UILabel()
|
||||||
|
lazy var translatedInfoView: UIView = {
|
||||||
|
let containerView = UIView()
|
||||||
|
|
||||||
|
let revertButton = UIButton()
|
||||||
|
revertButton.setTitle("Show Original", for: .normal)
|
||||||
|
revertButton.setTitleColor(Asset.Colors.brand.color, for: .normal)
|
||||||
|
revertButton.addAction(UIAction { [weak self] _ in
|
||||||
|
self?.revertTranslation()
|
||||||
|
}, for: .touchUpInside)
|
||||||
|
|
||||||
|
[containerView, translatedInfoLabel, revertButton].forEach {
|
||||||
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
}
|
||||||
|
|
||||||
|
[translatedInfoLabel, revertButton].forEach {
|
||||||
|
containerView.addSubview($0)
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerView.heightAnchor.constraint(equalToConstant: 20),
|
||||||
|
translatedInfoLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
||||||
|
translatedInfoLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
|
||||||
|
revertButton.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
||||||
|
revertButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16)
|
||||||
|
])
|
||||||
|
|
||||||
|
containerView.isHidden = true
|
||||||
|
|
||||||
|
return containerView
|
||||||
|
}()
|
||||||
|
|
||||||
// toolbar
|
// toolbar
|
||||||
let actionToolbarAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
let actionToolbarAdaptiveMarginContainerView = AdaptiveMarginContainerView()
|
||||||
|
@ -217,6 +248,7 @@ public final class StatusView: UIView {
|
||||||
setMediaDisplay(isDisplay: false)
|
setMediaDisplay(isDisplay: false)
|
||||||
setPollDisplay(isDisplay: false)
|
setPollDisplay(isDisplay: false)
|
||||||
setFilterHintLabelDisplay(isDisplay: false)
|
setFilterHintLabelDisplay(isDisplay: false)
|
||||||
|
setupTranslationIndicator()
|
||||||
}
|
}
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
|
@ -275,16 +307,6 @@ extension StatusView {
|
||||||
|
|
||||||
// statusMetricView
|
// statusMetricView
|
||||||
statusMetricView.delegate = self
|
statusMetricView.delegate = self
|
||||||
|
|
||||||
// status translation
|
|
||||||
viewModel.$isTranslated.sink { [weak self] isTranslated in
|
|
||||||
guard
|
|
||||||
let self = self,
|
|
||||||
let status = self.viewModel.originalStatus
|
|
||||||
else { return }
|
|
||||||
self.configureTranslated(status: status)
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -448,6 +470,9 @@ extension StatusView.Style {
|
||||||
statusView.filterHintLabel.centerXAnchor.constraint(equalTo: statusView.containerStackView.centerXAnchor),
|
statusView.filterHintLabel.centerXAnchor.constraint(equalTo: statusView.containerStackView.centerXAnchor),
|
||||||
statusView.filterHintLabel.centerYAnchor.constraint(equalTo: statusView.containerStackView.centerYAnchor),
|
statusView.filterHintLabel.centerYAnchor.constraint(equalTo: statusView.containerStackView.centerYAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// translated info
|
||||||
|
statusView.containerStackView.addArrangedSubview(statusView.translatedInfoView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func inline(statusView: StatusView) {
|
func inline(statusView: StatusView) {
|
||||||
|
@ -660,6 +685,23 @@ extension StatusView: MastodonMenuDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StatusView {
|
||||||
|
func setupTranslationIndicator() {
|
||||||
|
viewModel.$translatedFromLanguage
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] translatedFromLanguage in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let translatedFromLanguage = translatedFromLanguage {
|
||||||
|
self.translatedInfoLabel.text = String(format: "Translated from %@", Locale.current.localizedString(forIdentifier: translatedFromLanguage) ?? "Unknown")
|
||||||
|
self.translatedInfoView.isHidden = false
|
||||||
|
} else {
|
||||||
|
self.translatedInfoView.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ extension MastodonMenu {
|
||||||
return deleteAction
|
return deleteAction
|
||||||
case let .translateStatus(context):
|
case let .translateStatus(context):
|
||||||
let translateAction = BuiltAction(
|
let translateAction = BuiltAction(
|
||||||
title: String(format: "Translate from %@", context.language),
|
title: String(format: "Translate from %@", Locale.current.localizedString(forIdentifier: context.language) ?? "Unknown"),
|
||||||
image: UIImage(systemName: "character.book.closed")
|
image: UIImage(systemName: "character.book.closed")
|
||||||
) { [weak delegate] in
|
) { [weak delegate] in
|
||||||
guard let delegate = delegate else { return }
|
guard let delegate = delegate else { return }
|
||||||
|
|
Loading…
Reference in New Issue