Merge pull request #1135 from mastodon/nuke_coredata_translations

Don't persist translations
This commit is contained in:
Nathan Mattes 2023-10-19 12:04:45 +02:00 committed by GitHub
commit a3733ea578
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 139 additions and 175 deletions

View File

@ -151,7 +151,7 @@ extension DataSourceFacade {
struct MenuContext {
let author: ManagedObjectRecord<MastodonUser>?
let status: ManagedObjectRecord<Status>?
let statusViewModel: StatusView.ViewModel?
let button: UIButton?
let barButtonItem: UIBarButtonItem?
}
@ -266,7 +266,7 @@ extension DataSourceFacade {
context: dependency.context,
authContext: dependency.authContext,
user: user,
status: menuContext.status
status: menuContext.statusViewModel?.originalStatus?.asRecord
)
_ = dependency.coordinator.present(
@ -297,7 +297,7 @@ extension DataSourceFacade {
)
case .bookmarkStatus:
Task {
guard let status = menuContext.status else {
guard let status = menuContext.statusViewModel?.originalStatus?.asRecord else {
assertionFailure()
return
}
@ -310,7 +310,7 @@ extension DataSourceFacade {
Task {
let managedObjectContext = dependency.context.managedObjectContext
guard let status: ManagedObjectRecord<Status> = try? await managedObjectContext.perform(block: {
guard let object = menuContext.status?.object(in: managedObjectContext) else { return nil }
guard let object = menuContext.statusViewModel?.originalStatus?.asRecord.object(in: managedObjectContext) else { return nil }
let objectID = (object.reblog ?? object).objectID
return .init(objectID: objectID)
}) else {
@ -344,7 +344,7 @@ extension DataSourceFacade {
style: .destructive
) { [weak dependency] _ in
guard let dependency = dependency else { return }
guard let status = menuContext.status else { return }
guard let status = menuContext.statusViewModel?.originalStatus?.asRecord else { return }
Task {
try await DataSourceFacade.responseToDeleteStatus(
dependency: dependency,
@ -358,12 +358,12 @@ extension DataSourceFacade {
dependency.present(alertController, animated: true)
case .translateStatus:
guard let status = menuContext.status else { return }
guard let status = menuContext.statusViewModel?.originalStatus?.asRecord else { return }
do {
try await DataSourceFacade.translateStatus(
provider: dependency,
status: status
)
let translation = try await DataSourceFacade.translateStatus(provider: dependency,status: status)
menuContext.statusViewModel?.translation = translation
} catch TranslationFailure.emptyOrInvalidResponse {
let alertController = UIAlertController(title: L10n.Common.Alerts.TranslationFailed.title, message: L10n.Common.Alerts.TranslationFailed.message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: L10n.Common.Alerts.TranslationFailed.button, style: .default))
@ -371,7 +371,7 @@ extension DataSourceFacade {
}
case .editStatus:
guard let status = menuContext.status?.object(in: dependency.context.managedObjectContext) else { return }
guard let status = menuContext.statusViewModel?.originalStatus?.asRecord.object(in: dependency.context.managedObjectContext) else { return }
let statusSource = try await dependency.context.apiService.getStatusSource(
forStatusID: status.id,

View File

@ -9,6 +9,7 @@ import UIKit
import CoreData
import CoreDataStack
import MastodonCore
import MastodonSDK
typealias Provider = UIViewController & NeedsDependency & AuthContextProvider
@ -20,26 +21,26 @@ extension DataSourceFacade {
public static func translateStatus(
provider: Provider,
status: ManagedObjectRecord<Status>
) async throws {
) async throws -> Mastodon.Entity.Translation? {
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
await selectionFeedbackGenerator.selectionChanged()
guard
let status = status.object(in: provider.context.managedObjectContext)
else {
return
return nil
}
if let reblog = status.reblog {
try await translateAndApply(provider: provider, status: reblog)
return try await translateStatus(provider: provider, status: reblog)
} else {
try await translateAndApply(provider: provider, status: status)
return try await translateStatus(provider: provider, status: status)
}
}
}
private extension DataSourceFacade {
static func translateStatus(provider: Provider, status: Status) async throws -> Status.TranslatedContent? {
static func translateStatus(provider: Provider, status: Status) async throws -> Mastodon.Entity.Translation? {
do {
let value = try await provider.context
.apiService
@ -49,22 +50,12 @@ private extension DataSourceFacade {
).value
guard let content = value.content else {
throw TranslationFailure.emptyOrInvalidResponse
return nil
}
return Status.TranslatedContent(content: content, provider: value.provider)
return value
} catch {
throw TranslationFailure.emptyOrInvalidResponse
}
}
static func translateAndApply(provider: Provider, status: Status) async throws {
do {
let translated = try await translateStatus(provider: provider, status: status)
status.update(translatedContent: translated)
} catch {
status.update(translatedContent: nil)
throw TranslationFailure.emptyOrInvalidResponse
}
}
}

View File

@ -44,7 +44,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
action: action,
menuContext: .init(
author: author,
status: nil,
statusViewModel: nil,
button: button,
barButtonItem: nil
)

View File

@ -498,13 +498,23 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
}
}
}
let statusViewModel: StatusView.ViewModel?
if let cell = cell as? StatusTableViewCell {
statusViewModel = await cell.statusView.viewModel
} else if let cell = cell as? StatusThreadRootTableViewCell {
statusViewModel = await cell.statusView.viewModel
} else {
statusViewModel = nil
}
try await DataSourceFacade.responseToMenuAction(
dependency: self,
action: action,
menuContext: .init(
author: author,
status: status,
statusViewModel: statusViewModel,
button: button,
barButtonItem: nil
)

View File

@ -894,7 +894,7 @@ extension ProfileViewController: MastodonMenuDelegate {
action: action,
menuContext: DataSourceFacade.MenuContext(
author: userRecord,
status: nil,
statusViewModel: nil,
button: nil,
barButtonItem: self.moreMenuBarButtonItem
)

View File

@ -90,8 +90,7 @@ extension StatusTableViewCell {
}
.store(in: &_disposeBag)
statusView.viewModel
.$translatedFromLanguage
statusView.viewModel.$translation
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] _ in
self?.invalidateIntrinsicContentSize()

View File

@ -76,8 +76,7 @@ extension StatusThreadRootTableViewCell {
statusView.contentMetaText.textView.isAccessibilityElement = true
statusView.contentMetaText.textView.isSelectable = true
statusView.viewModel
.$translatedFromLanguage
statusView.viewModel.$translation
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] _ in
self?.invalidateIntrinsicContentSize()

View File

@ -10,17 +10,7 @@ import Foundation
public final class Status: NSManagedObject {
public typealias ID = String
public class TranslatedContent: NSObject {
public let content: String
public let provider: String?
public init(content: String, provider: String?) {
self.content = content
self.provider = provider
}
}
// sourcery: autoGenerateProperty
@NSManaged public private(set) var identifier: ID
// sourcery: autoGenerateProperty
@ -118,9 +108,6 @@ public final class Status: NSManagedObject {
@NSManaged public private(set) var deletedAt: Date?
// sourcery: autoUpdatableObject
@NSManaged public private(set) var revealedAt: Date?
// sourcery: autoUpdatableObject
@NSManaged public private(set) var translatedContent: TranslatedContent?
}
extension Status {
@ -535,11 +522,6 @@ extension Status: AutoUpdatableObject {
self.revealedAt = revealedAt
}
}
public func update(translatedContent: TranslatedContent?) {
if self.translatedContent != translatedContent {
self.translatedContent = translatedContent
}
}
public func update(attachments: [MastodonAttachment]) {
if self.attachments != attachments {
self.attachments = attachments

View File

@ -49,7 +49,7 @@ extension Instance {
version?.majorServerVersion(greaterThanOrEquals: 4) ?? false // following Tags is support beginning with Mastodon v4.0.0
}
var isTranslationEnabled: Bool {
public var isTranslationEnabled: Bool {
if let configuration = configurationV2 {
return configuration.translation?.enabled == true
}

View File

@ -84,12 +84,14 @@ public struct MastodonAuthentication: Codable, Hashable {
}
public func instance(in context: NSManagedObjectContext) -> Instance? {
guard
let instanceObjectIdURI = instanceObjectIdURI,
let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: instanceObjectIdURI)
else { return nil }
return try? context.existingObject(with: objectID) as? Instance
guard let instanceObjectIdURI,
let objectID = context.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: instanceObjectIdURI)
else {
return nil
}
let instance = try? context.existingObject(with: objectID) as? Instance
return instance
}
public func user(in context: NSManagedObjectContext) -> MastodonUser? {

View File

@ -19,12 +19,14 @@ extension APIService {
) async throws -> Mastodon.Response.Content<Mastodon.Entity.Translation> {
let domain = authenticationBox.domain
let authorization = authenticationBox.userAuthorization
let targetLanguage = Bundle.main.preferredLocalizations.first
let response = try await Mastodon.API.Statuses.translate(
session: session,
domain: domain,
statusID: statusID,
authorization: authorization
authorization: authorization,
targetLanguage: targetLanguage
).singleOutput()
return response

View File

@ -16,7 +16,11 @@ extension Mastodon.API.Statuses {
.appendingPathComponent(statusID)
.appendingPathComponent("translate")
}
public struct TranslateQuery: Codable, PostQuery {
public let lang: String
}
/// Translate Status
///
/// Translate a given Status.
@ -31,11 +35,21 @@ extension Mastodon.API.Statuses {
session: URLSession,
domain: String,
statusID: Mastodon.Entity.Status.ID,
authorization: Mastodon.API.OAuth.Authorization?
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Translation>, Error> {
authorization: Mastodon.API.OAuth.Authorization?,
targetLanguage: String?
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Translation>, Error> {
let query: TranslateQuery?
if let targetLanguage {
query = TranslateQuery(lang: targetLanguage)
} else {
query = nil
}
let request = Mastodon.API.post(
url: translateEndpointURL(domain: domain, statusID: statusID),
query: nil,
query: query,
authorization: authorization
)
return session.dataTaskPublisher(for: request)

View File

@ -221,38 +221,27 @@ extension NotificationView.ViewModel {
)
)
.sink { [weak self] authorName, isMuting, isBlocking, isMyselfIsTranslatedIsFollowed in
guard let name = authorName?.string else {
guard let name = authorName?.string, let self, let context = self.context, let authContext = self.authContext else {
notificationView.menuButton.menu = nil
return
}
let (isMyself, isTranslated, isFollowed) = isMyselfIsTranslatedIsFollowed
lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? = {
guard
let self = self,
let context = self.context,
let authContext = self.authContext
else { return nil }
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
let authentication = authContext.mastodonAuthenticationBox.authentication
configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2
}
return configuration
}()
let menuContext = NotificationView.AuthorMenuContext(
let (isMyself, isTranslated, isFollowed) = isMyselfIsTranslatedIsFollowed
let authentication = authContext.mastodonAuthenticationBox.authentication
let instance = authentication.instance(in: context.managedObjectContext)
let isTranslationEnabled = instance?.isTranslationEnabled ?? false
let menuContext = NotificationView.AuthorMenuContext(
name: name,
isMuting: isMuting,
isBlocking: isBlocking,
isMyself: isMyself,
isBookmarking: false, // no bookmark action display for notification item
isFollowed: isFollowed,
isTranslationEnabled: instanceConfigurationV2?.translation?.enabled == true,
isTranslationEnabled: isTranslationEnabled,
isTranslated: isTranslated,
statusLanguage: ""
statusLanguage: nil
)
let (menu, actions) = notificationView.setupAuthorMenu(menuContext: menuContext)
notificationView.menuButton.menu = menu

View File

@ -179,7 +179,10 @@ extension StatusAuthorView {
postActions.append(.editStatus)
}
if let statusLanguage = menuContext.statusLanguage, menuContext.isTranslationEnabled {
if menuContext.isTranslationEnabled,
let statusLanguage = menuContext.statusLanguage,
let deviceLanguage = Bundle.main.preferredLocalizations.first,
deviceLanguage != statusLanguage {
if menuContext.isTranslated == false {
postActions.append(.translateStatus(.init(language: statusLanguage)))
} else {

View File

@ -85,13 +85,10 @@ extension StatusView {
configureToolbar(status: status)
configureFilter(status: status)
viewModel.originalStatus = status
[
status.publisher(for: \.translatedContent),
status.reblog?.publisher(for: \.translatedContent)
].compactMap { $0 }
.last?
viewModel.$translation
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
.sink { [weak self] translation in
self?.configureTranslated(status: status)
}
.store(in: &disposeBag)
@ -293,36 +290,22 @@ extension StatusView {
public func revertTranslation() {
guard let originalStatus = viewModel.originalStatus else { return }
viewModel.translatedFromLanguage = nil
viewModel.translatedUsingProvider = nil
originalStatus.reblog?.update(translatedContent: nil)
originalStatus.update(translatedContent: nil)
viewModel.translation = nil
configure(status: originalStatus)
}
func configureTranslated(status: Status) {
let translatedContent: Status.TranslatedContent? = {
if let translatedContent = status.reblog?.translatedContent {
return translatedContent
}
return status.translatedContent
}()
guard
let translatedContent = translatedContent
else {
guard let translation = viewModel.translation,
let translatedContent = translation.content else {
viewModel.isCurrentlyTranslating = false
return
}
// content
do {
let content = MastodonContent(content: translatedContent.content, emojis: status.emojis.asDictionary)
let content = MastodonContent(content: translatedContent, emojis: status.emojis.asDictionary)
let metaContent = try MastodonMetaContent.convert(document: content)
viewModel.content = metaContent
viewModel.translatedFromLanguage = status.reblog?.language ?? status.language
viewModel.translatedUsingProvider = status.reblog?.translatedContent?.provider ?? status.translatedContent?.provider
viewModel.isCurrentlyTranslating = false
} catch {
assertionFailure(error.localizedDescription)
@ -342,7 +325,6 @@ extension StatusView {
let content = MastodonContent(content: statusEdit.content, emojis: statusEdit.emojis.asDictionary)
let metaContent = try MastodonMetaContent.convert(document: content)
viewModel.content = metaContent
viewModel.translatedFromLanguage = nil
viewModel.isCurrentlyTranslating = false
} catch {
assertionFailure(error.localizedDescription)
@ -351,7 +333,7 @@ extension StatusView {
}
private func configureContent(status: Status) {
guard status.translatedContent == nil else {
guard viewModel.translation == nil else {
return configureTranslated(status: status)
}
@ -377,7 +359,6 @@ extension StatusView {
let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary)
let metaContent = try MastodonMetaContent.convert(document: content)
viewModel.content = metaContent
viewModel.translatedFromLanguage = nil
viewModel.isCurrentlyTranslating = false
} catch {
assertionFailure(error.localizedDescription)

View File

@ -46,8 +46,7 @@ extension StatusView {
// Translation
@Published public var isCurrentlyTranslating = false
@Published public var translatedFromLanguage: String?
@Published public var translatedUsingProvider: String?
@Published public var translation: Mastodon.Entity.Translation? = nil
@Published public var timestamp: Date?
public var timestampFormatter: ((_ date: Date, _ isEdited: Bool) -> String)?
@ -148,10 +147,9 @@ extension StatusView {
isContentSensitive = false
isMediaSensitive = false
isSensitiveToggled = false
translatedFromLanguage = nil
translatedUsingProvider = nil
isCurrentlyTranslating = false
translation = nil
activeFilters = []
filterContext = nil
}
@ -657,60 +655,49 @@ extension StatusView.ViewModel {
$isFollowed
)
let publishersThree = Publishers.CombineLatest(
$translatedFromLanguage,
$translation,
$language
)
Publishers.CombineLatest3(
publisherOne.eraseToAnyPublisher(),
publishersTwo.eraseToAnyPublisher(),
publishersThree.eraseToAnyPublisher()
).eraseToAnyPublisher()
.sink { tupleOne, tupleTwo, tupleThree in
let (authorName, isMyself) = tupleOne
let (isMuting, isBlocking, isBookmark, isFollowed) = tupleTwo
let (translatedFromLanguage, language) = tupleThree
guard let name = authorName?.string else {
statusView.authorView.menuButton.menu = nil
return
}
lazy var instanceConfigurationV2: Mastodon.Entity.V2.Instance.Configuration? = {
guard
let context = self.context,
let authContext = self.authContext
else {
return nil
.sink { tupleOne, tupleTwo, tupleThree in
let (authorName, isMyself) = tupleOne
let (isMuting, isBlocking, isBookmark, isFollowed) = tupleTwo
let (translatedFromLanguage, language) = tupleThree
guard let name = authorName?.string, let context = self.context, let authContext = self.authContext else {
statusView.authorView.menuButton.menu = nil
return
}
let authentication = authContext.mastodonAuthenticationBox.authentication
let instance = authentication.instance(in: context.managedObjectContext)
let isTranslationEnabled = instance?.isTranslationEnabled ?? false
let menuContext = StatusAuthorView.AuthorMenuContext(
name: name,
isMuting: isMuting,
isBlocking: isBlocking,
isMyself: isMyself,
isBookmarking: isBookmark,
isFollowed: isFollowed,
isTranslationEnabled: isTranslationEnabled,
isTranslated: translatedFromLanguage != nil,
statusLanguage: language
)
var configuration: Mastodon.Entity.V2.Instance.Configuration? = nil
context.managedObjectContext.performAndWait {
let authentication = authContext.mastodonAuthenticationBox.authentication
configuration = authentication.instance(in: context.managedObjectContext)?.configurationV2
}
return configuration
}()
let menuContext = StatusAuthorView.AuthorMenuContext(
name: name,
isMuting: isMuting,
isBlocking: isBlocking,
isMyself: isMyself,
isBookmarking: isBookmark,
isFollowed: isFollowed,
isTranslationEnabled: instanceConfigurationV2?.translation?.enabled == true,
isTranslated: translatedFromLanguage != nil,
statusLanguage: language
)
let (menu, actions) = authorView.setupAuthorMenu(menuContext: menuContext)
authorView.menuButton.menu = menu
authorView.authorActions = actions
authorView.menuButton.showsMenuAsPrimaryAction = true
}
.store(in: &disposeBag)
let (menu, actions) = authorView.setupAuthorMenu(menuContext: menuContext)
authorView.menuButton.menu = menu
authorView.authorActions = actions
authorView.menuButton.showsMenuAsPrimaryAction = true
}
.store(in: &disposeBag)
}
private func bindFilter(statusView: StatusView) {
$isFiltered
.sink { isFiltered in
@ -877,15 +864,20 @@ extension StatusView.ViewModel {
.assign(to: \.toolbarActions, on: statusView)
.store(in: &disposeBag)
let translatedFromLabel = Publishers.CombineLatest($translatedFromLanguage, $translatedUsingProvider)
.map { (language, provider) -> String? in
if let language {
return L10n.Common.Controls.Status.Translation.translatedFrom(
Locale.current.localizedString(forIdentifier: language) ?? L10n.Common.Controls.Status.Translation.unknownLanguage,
provider ?? L10n.Common.Controls.Status.Translation.unknownProvider
)
let translatedFromLabel = $translation
.map { translation -> String? in
guard let translation else { return nil }
let provider = translation.provider ?? L10n.Common.Controls.Status.Translation.unknownProvider
let sourceLanguage: String
if let language = translation.sourceLanguage {
sourceLanguage = Locale.current.localizedString(forIdentifier: language) ?? L10n.Common.Controls.Status.Translation.unknownLanguage
} else {
sourceLanguage = L10n.Common.Controls.Status.Translation.unknownLanguage
}
return nil
return L10n.Common.Controls.Status.Translation.translatedFrom(sourceLanguage, provider)
}
translatedFromLabel

View File

@ -668,7 +668,7 @@ extension StatusView {
}
private var hideTranslationAction: UIAccessibilityCustomAction? {
guard viewModel.translatedFromLanguage != nil else { return nil }
guard viewModel.translation?.sourceLanguage != nil else { return nil }
return UIAccessibilityCustomAction(name: L10n.Common.Controls.Status.Translation.showOriginal) { [weak self] _ in
self?.revertTranslation()
return true