Implement "Default Post Language" and "Ask Before"-Alerts (#1240)
* Implement Settings->General->"Ask Before" and add "Ask Before Posting Without Alt Text" IOS-166 * Implement Alt Missing Alert for Status Edits (IOS-166) * Fix status edit composes duplicate message * Show (or don't) the "Really delete post?" Alert based on the User's preference (IOS-166) * Implement alert for boost/unboost (IOS-166) * Begin implementing "Default Post Language"-Setting (IOS-166) * Show "Unfollow @user?" Alert (IOS-166) * Merge conflict fixes for IOS-166 * Implement default post language setting (IOS-166) * Fix follow button state not updated correctly (IOS-166) * Add PR feedback (IOS-166) * Improve default language cell style (IOS-166) * Fix language filter broken (IOS-166)
This commit is contained in:
parent
d3ffa3782e
commit
2e7054cb68
|
@ -52,6 +52,24 @@
|
|||
"title": "Note",
|
||||
"message": "Translation failed. Maybe the administrator has not enabled translations on this server or this server is running an older version of Mastodon where translations are not yet supported.",
|
||||
"button": "OK"
|
||||
},
|
||||
"media_missing_alt_text": {
|
||||
"title": "Media Missing Alt Text",
|
||||
"message": "%d of your images are missing alt text.\nPost Anyway?",
|
||||
"cancel": "Cancel",
|
||||
"post": "Post"
|
||||
},
|
||||
"boost_a_post": {
|
||||
"title_boost": "Boost Post?",
|
||||
"title_unboost": "Unboost Post?",
|
||||
"cancel": "Cancel",
|
||||
"boost": "Boost",
|
||||
"unboost": "Unboost"
|
||||
},
|
||||
"unfollow_user": {
|
||||
"title": "Unfollow %@?",
|
||||
"cancel": "Cancel",
|
||||
"unfollow": "Unfollow"
|
||||
}
|
||||
},
|
||||
"controls": {
|
||||
|
@ -751,10 +769,21 @@
|
|||
"light": "Light",
|
||||
"system": "Use Device Appearance"
|
||||
},
|
||||
"ask_before": {
|
||||
"section_title": "Ask before…",
|
||||
"posting_without_alt_text": "Posting without Alt Text",
|
||||
"unfollowing_someone": "Unfollowing Someone",
|
||||
"boosting_a_post": "Boosting a Post",
|
||||
"deleting_a_post": "Deleting a Post"
|
||||
},
|
||||
"design": {
|
||||
"section_title": "Design",
|
||||
"show_animations": "Play Animated Avatars and Emoji"
|
||||
},
|
||||
"language": {
|
||||
"section_title": "Language",
|
||||
"default_post_language": "Default Post Language"
|
||||
},
|
||||
"links": {
|
||||
"section_title": "Links",
|
||||
"open_in_mastodon": "Open in Mastodon",
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
2A409F832B5955290044E472 /* MastodonStatusThreadViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A409F822B5955290044E472 /* MastodonStatusThreadViewModel+State.swift */; };
|
||||
2A506CF4292CD85800059C37 /* FollowedTagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */; };
|
||||
2A506CF6292D040100059C37 /* HashtagTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A506CF5292D040100059C37 /* HashtagTimelineHeaderView.swift */; };
|
||||
2A631AE82B8C9F6600FE0778 /* LanguagePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A631AE72B8C9F6600FE0778 /* LanguagePickerViewController.swift */; };
|
||||
2A64515E29642A8A00CD8B8A /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */; };
|
||||
2A64516929642A8B00CD8B8A /* OpenInActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
2A71F541296DBDA80049F54A /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A71F53D296DBDA80049F54A /* Media.xcassets */; };
|
||||
|
@ -642,6 +643,7 @@
|
|||
2A409F822B5955290044E472 /* MastodonStatusThreadViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonStatusThreadViewModel+State.swift"; sourceTree = "<group>"; };
|
||||
2A506CF3292CD85800059C37 /* FollowedTagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedTagsViewController.swift; sourceTree = "<group>"; };
|
||||
2A506CF5292D040100059C37 /* HashtagTimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineHeaderView.swift; sourceTree = "<group>"; };
|
||||
2A631AE72B8C9F6600FE0778 /* LanguagePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePickerViewController.swift; sourceTree = "<group>"; };
|
||||
2A6451022964223800CD8B8A /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
|
||||
2A64515D29642A8A00CD8B8A /* OpenInActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenInActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2A71F53D296DBDA80049F54A /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
|
||||
|
@ -1474,6 +1476,14 @@
|
|||
path = FollowedTags;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2A631AE62B8C9F5900FE0778 /* Language */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2A631AE72B8C9F6600FE0778 /* LanguagePickerViewController.swift */,
|
||||
);
|
||||
path = Language;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2A71F53C296DBDA80049F54A /* OpenInActionExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1947,6 +1957,7 @@
|
|||
D8F917042A4B0657008A5370 /* General Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2A631AE62B8C9F5900FE0778 /* Language */,
|
||||
D8318A832A4468A800C0FB73 /* GeneralSettingsViewController.swift */,
|
||||
D8F917052A4B0791008A5370 /* GeneralSettings.swift */,
|
||||
D8F917102A4C6B40008A5370 /* GeneralSettingToggleTableViewCell.swift */,
|
||||
|
@ -3931,6 +3942,7 @@
|
|||
DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */,
|
||||
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */,
|
||||
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
|
||||
2A631AE82B8C9F6600FE0778 /* LanguagePickerViewController.swift in Sources */,
|
||||
DBB45B5B27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift in Sources */,
|
||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||
DB4932B926F31AD300EF46D4 /* BadgeButton.swift in Sources */,
|
||||
|
|
|
@ -12,25 +12,67 @@ import MastodonSDK
|
|||
import MastodonLocalization
|
||||
|
||||
extension DataSourceFacade {
|
||||
@MainActor
|
||||
static func responseToUserFollowAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
dependency: ViewControllerWithDependencies & AuthContextProvider,
|
||||
account: Mastodon.Entity.Account
|
||||
) async throws -> Mastodon.Entity.Relationship {
|
||||
let selectionFeedbackGenerator = await UISelectionFeedbackGenerator()
|
||||
await selectionFeedbackGenerator.selectionChanged()
|
||||
let authBox = dependency.authContext.mastodonAuthenticationBox
|
||||
let relationship = try await dependency.context.apiService.relationship(
|
||||
forAccounts: [account], authenticationBox: authBox
|
||||
).value.first
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
Task { @MainActor in
|
||||
let performAction = {
|
||||
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
|
||||
selectionFeedbackGenerator.selectionChanged()
|
||||
|
||||
let response = try await dependency.context.apiService.toggleFollow(
|
||||
account: account,
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
).value
|
||||
let response = try await dependency.context.apiService.toggleFollow(
|
||||
account: account,
|
||||
authenticationBox: dependency.authContext.mastodonAuthenticationBox
|
||||
).value
|
||||
|
||||
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
||||
dependency.context.authenticationService.fetchFollowingAndBlockedAsync()
|
||||
|
||||
|
||||
NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [
|
||||
UserInfoKey.relationship: response
|
||||
])
|
||||
NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [
|
||||
UserInfoKey.relationship: response
|
||||
])
|
||||
|
||||
continuation.resume(returning: response)
|
||||
}
|
||||
|
||||
return response
|
||||
if relationship?.following == true {
|
||||
let alert = UIAlertController(
|
||||
title: L10n.Common.Alerts.UnfollowUser.title("@\(account.username)"),
|
||||
message: nil,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let cancel = UIAlertAction(title: L10n.Common.Alerts.UnfollowUser.cancel, style: .default) { _ in
|
||||
if let relationship {
|
||||
NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [
|
||||
UserInfoKey.relationship: relationship
|
||||
])
|
||||
|
||||
continuation.resume(returning: relationship)
|
||||
} else {
|
||||
continuation.resume(throwing: AppError.unexpected)
|
||||
}
|
||||
}
|
||||
alert.addAction(cancel)
|
||||
let unfollow = UIAlertAction(title: L10n.Common.Alerts.UnfollowUser.unfollow, style: .destructive) { _ in
|
||||
Task {
|
||||
try await performAction()
|
||||
}
|
||||
}
|
||||
alert.addAction(unfollow)
|
||||
dependency.present(alert, animated: true)
|
||||
} else {
|
||||
try await performAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,12 +9,43 @@ import UIKit
|
|||
import MastodonCore
|
||||
import MastodonUI
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
|
||||
extension DataSourceFacade {
|
||||
@MainActor
|
||||
static func responseToStatusReblogAction(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: MastodonStatus
|
||||
) async throws {
|
||||
if UserDefaults.shared.askBeforeBoostingAPost {
|
||||
let alertController = UIAlertController(
|
||||
title: status.entity.reblogged == true ? L10n.Common.Alerts.BoostAPost.titleUnboost : L10n.Common.Alerts.BoostAPost.titleBoost,
|
||||
message: nil,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Alerts.BoostAPost.cancel, style: .default)
|
||||
alertController.addAction(cancelAction)
|
||||
let confirmAction = UIAlertAction(
|
||||
title: status.entity.reblogged == true ? L10n.Common.Alerts.BoostAPost.unboost : L10n.Common.Alerts.BoostAPost.boost,
|
||||
style: .default
|
||||
) { _ in
|
||||
Task { @MainActor in
|
||||
try? await performReblog(provider: provider, status: status)
|
||||
}
|
||||
}
|
||||
alertController.addAction(confirmAction)
|
||||
provider.present(alertController, animated: true)
|
||||
} else {
|
||||
try await performReblog(provider: provider, status: status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension DataSourceFacade {
|
||||
@MainActor
|
||||
static func performReblog(
|
||||
provider: DataSourceProvider & AuthContextProvider,
|
||||
status: MastodonStatus
|
||||
) async throws {
|
||||
let selectionFeedbackGenerator = UISelectionFeedbackGenerator()
|
||||
selectionFeedbackGenerator.selectionChanged()
|
||||
|
|
|
@ -295,29 +295,28 @@ extension DataSourceFacade {
|
|||
transition: .activityViewControllerPresent(animated: true, completion: nil)
|
||||
)
|
||||
case .deleteStatus:
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Common.Alerts.DeletePost.title,
|
||||
message: L10n.Common.Alerts.DeletePost.message,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let confirmAction = UIAlertAction(
|
||||
title: L10n.Common.Controls.Actions.delete,
|
||||
style: .destructive
|
||||
) { [weak dependency] _ in
|
||||
guard let dependency = dependency else { return }
|
||||
if UserDefaults.shared.askBeforeDeletingAPost {
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Common.Alerts.DeletePost.title,
|
||||
message: L10n.Common.Alerts.DeletePost.message,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let confirmAction = UIAlertAction(
|
||||
title: L10n.Common.Controls.Actions.delete,
|
||||
style: .destructive
|
||||
) { [weak dependency] _ in
|
||||
guard let dependency else { return }
|
||||
guard let status = menuContext.statusViewModel?.originalStatus else { return }
|
||||
performDeletion(of: status, with: dependency)
|
||||
}
|
||||
alertController.addAction(confirmAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
|
||||
alertController.addAction(cancelAction)
|
||||
dependency.present(alertController, animated: true)
|
||||
} else {
|
||||
guard let status = menuContext.statusViewModel?.originalStatus else { return }
|
||||
Task {
|
||||
try await DataSourceFacade.responseToDeleteStatus(
|
||||
dependency: dependency,
|
||||
status: status
|
||||
)
|
||||
} // end Task
|
||||
performDeletion(of: status, with: dependency)
|
||||
}
|
||||
alertController.addAction(confirmAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
|
||||
alertController.addAction(cancelAction)
|
||||
dependency.present(alertController, animated: true)
|
||||
|
||||
case .translateStatus:
|
||||
guard let status = menuContext.statusViewModel?.originalStatus else { return }
|
||||
|
||||
|
@ -406,3 +405,13 @@ extension DataSourceFacade {
|
|||
|
||||
}
|
||||
|
||||
private extension DataSourceFacade {
|
||||
static func performDeletion(of status: MastodonStatus, with dependency: NeedsDependency & AuthContextProvider & DataSourceProvider) {
|
||||
Task {
|
||||
try await DataSourceFacade.responseToDeleteStatus(
|
||||
dependency: dependency,
|
||||
status: status
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import MastodonSDK
|
|||
|
||||
extension DataSourceFacade {
|
||||
static func responseToUserViewButtonAction(
|
||||
dependency: NeedsDependency & AuthContextProvider,
|
||||
dependency: ViewControllerWithDependencies & AuthContextProvider,
|
||||
account: Mastodon.Entity.Account,
|
||||
buttonState: UserView.ButtonState
|
||||
) async throws {
|
||||
|
|
|
@ -195,6 +195,12 @@ extension ComposeViewController {
|
|||
|
||||
extension ComposeViewController {
|
||||
|
||||
private var mediaAttachmentViewModelsWithoutCaption: [AttachmentViewModel] {
|
||||
get {
|
||||
composeContentViewModel.attachmentViewModels.filter({ $0.caption.isEmpty })
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
guard composeContentViewModel.shouldDismiss else {
|
||||
showDismissConfirmAlertController()
|
||||
|
@ -215,12 +221,30 @@ extension ComposeViewController {
|
|||
return
|
||||
}
|
||||
|
||||
let attachmentsWithoutCaptionCount = mediaAttachmentViewModelsWithoutCaption.count
|
||||
|
||||
if UserDefaults.shared.askBeforePostingWithoutAltText && attachmentsWithoutCaptionCount > 0 {
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Common.Alerts.MediaMissingAltText.title,
|
||||
message: L10n.Common.Alerts.MediaMissingAltText.message(attachmentsWithoutCaptionCount),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Alerts.MediaMissingAltText.cancel, style: .default, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
let confirmAction = UIAlertAction(title: L10n.Common.Alerts.MediaMissingAltText.post, style: .default) { [weak self] action in
|
||||
self?.enqueuePublishStatus()
|
||||
}
|
||||
alertController.addAction(confirmAction)
|
||||
_ = coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil))
|
||||
return
|
||||
}
|
||||
|
||||
enqueuePublishStatus()
|
||||
}
|
||||
|
||||
private func enqueuePublishStatus() {
|
||||
do {
|
||||
let statusPublisher = try composeContentViewModel.statusPublisher()
|
||||
// let result = try await statusPublisher.publish(api: context.apiService, authContext: viewModel.authContext)
|
||||
// if let reactor = presentingViewController?.topMostNotModal as? StatusPublisherReactor {
|
||||
// statusPublisher.reactor = reactor
|
||||
// }
|
||||
viewModel.context.publisherService.enqueue(
|
||||
statusPublisher: statusPublisher,
|
||||
authContext: viewModel.authContext
|
||||
|
@ -246,6 +270,28 @@ extension ComposeViewController {
|
|||
return
|
||||
}
|
||||
|
||||
let attachmentsWithoutCaptionCount = mediaAttachmentViewModelsWithoutCaption.count
|
||||
|
||||
if UserDefaults.shared.askBeforePostingWithoutAltText && attachmentsWithoutCaptionCount > 0 {
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Common.Alerts.MediaMissingAltText.title,
|
||||
message: L10n.Common.Alerts.MediaMissingAltText.message(attachmentsWithoutCaptionCount),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Alerts.MediaMissingAltText.cancel, style: .default, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
let confirmAction = UIAlertAction(title: L10n.Common.Alerts.MediaMissingAltText.post, style: .default) { [weak self] action in
|
||||
self?.enqueuePublishStatusEdit()
|
||||
}
|
||||
alertController.addAction(confirmAction)
|
||||
_ = coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil))
|
||||
return
|
||||
}
|
||||
|
||||
enqueuePublishStatusEdit()
|
||||
}
|
||||
|
||||
private func enqueuePublishStatusEdit() {
|
||||
do {
|
||||
guard let editStatusPublisher = try composeContentViewModel.statusEditPublisher() else { return }
|
||||
viewModel.context.publisherService.enqueue(
|
||||
|
@ -259,7 +305,6 @@ extension ComposeViewController {
|
|||
}
|
||||
|
||||
dismiss(animated: true, completion: nil)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,18 +2,23 @@
|
|||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonUI
|
||||
|
||||
class GeneralSettingSelectionCell: UITableViewCell {
|
||||
static let reuseIdentifier = "GeneralSettingSelectionCell"
|
||||
|
||||
func configure(with setting: GeneralSetting, viewModel: GeneralSettingsViewModel) {
|
||||
switch setting {
|
||||
case .appearance(let appearanceSetting):
|
||||
case let .appearance(appearanceSetting):
|
||||
configureAppearanceSetting(appearanceSetting: appearanceSetting, viewModel: viewModel)
|
||||
case .design(_):
|
||||
case .askBefore:
|
||||
assertionFailure("Not required here")
|
||||
case .design:
|
||||
// only for appearance and open links
|
||||
assertionFailure("Wrong Setting!")
|
||||
case .openLinksIn(let openLinkSetting):
|
||||
case let .language(setting):
|
||||
configureLanguageSetting(setting, viewModel: viewModel)
|
||||
case let .openLinksIn(openLinkSetting):
|
||||
configureOpenLinkSetting(openLinkSetting: openLinkSetting, viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
|
@ -45,5 +50,26 @@ class GeneralSettingSelectionCell: UITableViewCell {
|
|||
|
||||
contentConfiguration = content
|
||||
}
|
||||
|
||||
private func configureLanguageSetting(_ setting: GeneralSetting.Language, viewModel: GeneralSettingsViewModel) {
|
||||
tintColor = Asset.Colors.Brand.blurple.color
|
||||
accessoryType = .disclosureIndicator
|
||||
|
||||
var content = defaultContentConfiguration()
|
||||
content.prefersSideBySideTextAndSecondaryText = true
|
||||
content.text = setting.title
|
||||
|
||||
if let text = LanguagePicker.availableLanguages().first(where: { $0.localeId == UserDefaults.shared.defaultPostLanguage })?.exonym {
|
||||
content.secondaryAttributedText = NSAttributedString(
|
||||
string: text,
|
||||
attributes: [
|
||||
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
|
||||
.foregroundColor: Asset.Colors.inactive.color
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
contentConfiguration = content
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -27,8 +27,21 @@ class GeneralSettingToggleTableViewCell: ToggleTableViewCell {
|
|||
self.setting = setting
|
||||
|
||||
switch setting {
|
||||
case .appearance(_), .openLinksIn(_):
|
||||
assertionFailure("Only for Design")
|
||||
case .appearance, .openLinksIn, .language:
|
||||
assertionFailure("Not required here")
|
||||
case let .askBefore(askBefore):
|
||||
label.text = askBefore.title
|
||||
|
||||
switch askBefore {
|
||||
case .postingWithoutAltText:
|
||||
toggle.isOn = UserDefaults.shared.askBeforePostingWithoutAltText
|
||||
case .unfollowingSomeone:
|
||||
toggle.isOn = UserDefaults.shared.askBeforeUnfollowingSomeone
|
||||
case .boostingAPost:
|
||||
toggle.isOn = UserDefaults.shared.askBeforeBoostingAPost
|
||||
case .deletingAPost:
|
||||
toggle.isOn = UserDefaults.shared.askBeforeDeletingAPost
|
||||
}
|
||||
case .design(let designSetting):
|
||||
label.text = designSetting.title
|
||||
|
||||
|
|
|
@ -10,15 +10,21 @@ struct GeneralSettingsSection: Hashable {
|
|||
|
||||
enum GeneralSettingsSectionType: Hashable {
|
||||
case appearance
|
||||
case askBefore
|
||||
case design
|
||||
case language
|
||||
case links
|
||||
|
||||
var sectionTitle: String {
|
||||
switch self {
|
||||
case .appearance:
|
||||
return L10n.Scene.Settings.General.Appearance.sectionTitle
|
||||
case .askBefore:
|
||||
return L10n.Scene.Settings.General.AskBefore.sectionTitle
|
||||
case .design:
|
||||
return L10n.Scene.Settings.General.Design.sectionTitle
|
||||
case .language:
|
||||
return L10n.Scene.Settings.General.Language.sectionTitle
|
||||
case .links:
|
||||
return L10n.Scene.Settings.General.Links.sectionTitle
|
||||
}
|
||||
|
@ -28,7 +34,9 @@ enum GeneralSettingsSectionType: Hashable {
|
|||
enum GeneralSetting: Hashable {
|
||||
|
||||
case appearance(Appearance)
|
||||
case askBefore(AskBefore)
|
||||
case design(Design)
|
||||
case language(Language)
|
||||
case openLinksIn(OpenLinksIn)
|
||||
|
||||
enum Appearance: Int, CaseIterable {
|
||||
|
@ -51,6 +59,27 @@ enum GeneralSetting: Hashable {
|
|||
.init(rawValue: rawValue) ?? .unspecified
|
||||
}
|
||||
}
|
||||
|
||||
enum AskBefore: Hashable {
|
||||
case postingWithoutAltText
|
||||
case unfollowingSomeone
|
||||
case boostingAPost
|
||||
case deletingAPost
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .postingWithoutAltText:
|
||||
return L10n.Scene.Settings.General.AskBefore.postingWithoutAltText
|
||||
case .unfollowingSomeone:
|
||||
return L10n.Scene.Settings.General.AskBefore.unfollowingSomeone
|
||||
case .boostingAPost:
|
||||
return L10n.Scene.Settings.General.AskBefore.boostingAPost
|
||||
case .deletingAPost:
|
||||
return L10n.Scene.Settings.General.AskBefore.deletingAPost
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Design: Hashable {
|
||||
case showAnimations
|
||||
|
@ -62,6 +91,17 @@ enum GeneralSetting: Hashable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Language: Hashable {
|
||||
case defaultPostLanguage
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .defaultPostLanguage:
|
||||
return L10n.Scene.Settings.General.Language.defaultPostLanguage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum OpenLinksIn: Hashable, CaseIterable {
|
||||
case mastodon
|
||||
|
|
|
@ -2,17 +2,25 @@
|
|||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
import CoreDataStack
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
|
||||
struct GeneralSettingsViewModel {
|
||||
var selectedAppearence: GeneralSetting.Appearance
|
||||
var playAnimations: Bool
|
||||
var selectedOpenLinks: GeneralSetting.OpenLinksIn
|
||||
var askBeforePostingWithoutAltText: Bool
|
||||
var askBeforeUnfollowingSomeone: Bool
|
||||
var askBeforeBoostingAPost: Bool
|
||||
var askBeforeDeletingAPost: Bool
|
||||
var defaultPostLanguage: String
|
||||
}
|
||||
|
||||
protocol GeneralSettingsViewControllerDelegate: AnyObject {
|
||||
func save(_ viewController: UIViewController, setting: Setting, viewModel: GeneralSettingsViewModel)
|
||||
func showLanguagePicker(_ viewModel: GeneralSettingsViewModel, onLanguageSelected: @escaping OnLanguageSelected)
|
||||
}
|
||||
|
||||
class GeneralSettingsViewController: UIViewController {
|
||||
|
@ -27,7 +35,7 @@ class GeneralSettingsViewController: UIViewController {
|
|||
|
||||
let sections: [GeneralSettingsSection]
|
||||
|
||||
init(setting: Setting) {
|
||||
init(appContext: AppContext, setting: Setting) {
|
||||
tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.register(GeneralSettingSelectionCell.self, forCellReuseIdentifier: GeneralSettingSelectionCell.reuseIdentifier)
|
||||
|
@ -39,9 +47,18 @@ class GeneralSettingsViewController: UIViewController {
|
|||
.appearance(.dark),
|
||||
.appearance(.system)
|
||||
]),
|
||||
GeneralSettingsSection(type: .askBefore, entries: [
|
||||
.askBefore(.postingWithoutAltText),
|
||||
.askBefore(.unfollowingSomeone),
|
||||
.askBefore(.boostingAPost),
|
||||
.askBefore(.deletingAPost)
|
||||
]),
|
||||
GeneralSettingsSection(type: .design, entries: [
|
||||
.design(.showAnimations)
|
||||
]),
|
||||
GeneralSettingsSection(type: .language, entries: [
|
||||
.language(.defaultPostLanguage)
|
||||
]),
|
||||
GeneralSettingsSection(type: .links, entries: [
|
||||
.openLinksIn(.mastodon),
|
||||
.openLinksIn(.browser),
|
||||
|
@ -58,7 +75,12 @@ class GeneralSettingsViewController: UIViewController {
|
|||
viewModel = GeneralSettingsViewModel(
|
||||
selectedAppearence: GeneralSetting.Appearance(rawValue: UserDefaults.shared.customUserInterfaceStyle.rawValue) ?? .system,
|
||||
playAnimations: playAnimations,
|
||||
selectedOpenLinks: openLinksIn
|
||||
selectedOpenLinks: openLinksIn,
|
||||
askBeforePostingWithoutAltText: UserDefaults.shared.askBeforePostingWithoutAltText,
|
||||
askBeforeUnfollowingSomeone: UserDefaults.shared.askBeforeUnfollowingSomeone,
|
||||
askBeforeBoostingAPost: UserDefaults.shared.askBeforeBoostingAPost,
|
||||
askBeforeDeletingAPost: UserDefaults.shared.askBeforeDeletingAPost,
|
||||
defaultPostLanguage: UserDefaults.shared.defaultPostLanguage
|
||||
)
|
||||
|
||||
self.setting = setting
|
||||
|
@ -75,6 +97,12 @@ class GeneralSettingsViewController: UIViewController {
|
|||
|
||||
selectionCell.configure(with: .appearance(setting), viewModel: self.viewModel)
|
||||
cell = selectionCell
|
||||
case .askBefore(let setting):
|
||||
guard let toggleCell = tableView.dequeueReusableCell(withIdentifier: GeneralSettingToggleTableViewCell.reuseIdentifier, for: indexPath) as? GeneralSettingToggleTableViewCell else { fatalError("WTF? Wrong Cell!") }
|
||||
toggleCell.configure(with: .askBefore(setting), viewModel: self.viewModel)
|
||||
toggleCell.delegate = self
|
||||
|
||||
cell = toggleCell
|
||||
case .design(let setting):
|
||||
guard let toggleCell = tableView.dequeueReusableCell(withIdentifier: GeneralSettingToggleTableViewCell.reuseIdentifier, for: indexPath) as? GeneralSettingToggleTableViewCell else { fatalError("WTF? Wrong Cell!") }
|
||||
|
||||
|
@ -82,6 +110,11 @@ class GeneralSettingsViewController: UIViewController {
|
|||
toggleCell.delegate = self
|
||||
|
||||
cell = toggleCell
|
||||
case let .language(setting):
|
||||
guard let selectionCell = tableView.dequeueReusableCell(withIdentifier: GeneralSettingSelectionCell.reuseIdentifier, for: indexPath) as? GeneralSettingSelectionCell else { fatalError("WTF? Wrong Cell!") }
|
||||
|
||||
selectionCell.configure(with: .language(setting), viewModel: self.viewModel)
|
||||
cell = selectionCell
|
||||
case .openLinksIn(let setting):
|
||||
guard let selectionCell = tableView.dequeueReusableCell(withIdentifier: GeneralSettingSelectionCell.reuseIdentifier, for: indexPath) as? GeneralSettingSelectionCell else { fatalError("WTF? Wrong Cell!") }
|
||||
|
||||
|
@ -126,25 +159,42 @@ extension GeneralSettingsViewController: UITableViewDelegate {
|
|||
let section = sections[indexPath.section].entries[indexPath.row]
|
||||
|
||||
switch section {
|
||||
case .appearance(let appearanceOption):
|
||||
viewModel.selectedAppearence = appearanceOption
|
||||
case .appearance(let appearanceOption):
|
||||
viewModel.selectedAppearence = appearanceOption
|
||||
|
||||
if let snapshot = tableViewDataSource?.snapshot() {
|
||||
tableViewDataSource?.applySnapshotUsingReloadData(snapshot)
|
||||
}
|
||||
|
||||
case .askBefore(let askBefore):
|
||||
guard let cell = tableView.cellForRow(at: indexPath) as? GeneralSettingToggleTableViewCell else { return}
|
||||
|
||||
let newValue = (cell.toggle.isOn == false)
|
||||
cell.toggle.setOn(newValue, animated: true)
|
||||
|
||||
toggle(cell, setting: .askBefore(askBefore), isOn: newValue)
|
||||
case .design(let design):
|
||||
guard let cell = tableView.cellForRow(at: indexPath) as? GeneralSettingToggleTableViewCell else { return}
|
||||
|
||||
let newValue = (cell.toggle.isOn == false)
|
||||
cell.toggle.setOn(newValue, animated: true)
|
||||
|
||||
toggle(cell, setting: .design(design), isOn: newValue)
|
||||
case .language:
|
||||
delegate?.showLanguagePicker(viewModel) { [weak self] language in
|
||||
guard let self else { return }
|
||||
viewModel.defaultPostLanguage = language
|
||||
UserDefaults.shared.defaultPostLanguage = language
|
||||
if let snapshot = tableViewDataSource?.snapshot() {
|
||||
tableViewDataSource?.applySnapshotUsingReloadData(snapshot)
|
||||
}
|
||||
case .design(let design):
|
||||
guard let cell = tableView.cellForRow(at: indexPath) as? GeneralSettingToggleTableViewCell else { return}
|
||||
}
|
||||
case .openLinksIn(let openLinksInOption):
|
||||
viewModel.selectedOpenLinks = openLinksInOption
|
||||
|
||||
let newValue = (cell.toggle.isOn == false)
|
||||
cell.toggle.setOn(newValue, animated: true)
|
||||
|
||||
toggle(cell, setting: .design(design), isOn: newValue)
|
||||
case .openLinksIn(let openLinksInOption):
|
||||
viewModel.selectedOpenLinks = openLinksInOption
|
||||
|
||||
if let snapshot = tableViewDataSource?.snapshot() {
|
||||
tableViewDataSource?.applySnapshotUsingReloadData(snapshot)
|
||||
}
|
||||
if let snapshot = tableViewDataSource?.snapshot() {
|
||||
tableViewDataSource?.applySnapshotUsingReloadData(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
@ -155,13 +205,24 @@ extension GeneralSettingsViewController: UITableViewDelegate {
|
|||
extension GeneralSettingsViewController: GeneralSettingToggleTableViewCellDelegate {
|
||||
func toggle(_ cell: GeneralSettingToggleTableViewCell, setting: GeneralSetting, isOn: Bool) {
|
||||
switch setting {
|
||||
case .appearance(_), .openLinksIn(_):
|
||||
assertionFailure("No toggle")
|
||||
case .design(let designSetting):
|
||||
switch designSetting {
|
||||
case .showAnimations:
|
||||
viewModel.playAnimations = isOn
|
||||
}
|
||||
case .appearance, .openLinksIn, .language:
|
||||
assertionFailure("No toggle")
|
||||
case let .askBefore(askBefore):
|
||||
switch askBefore {
|
||||
case .postingWithoutAltText:
|
||||
UserDefaults.shared.askBeforePostingWithoutAltText = isOn
|
||||
case .unfollowingSomeone:
|
||||
UserDefaults.shared.askBeforeUnfollowingSomeone = isOn
|
||||
case .boostingAPost:
|
||||
UserDefaults.shared.askBeforeBoostingAPost = isOn
|
||||
case .deletingAPost:
|
||||
UserDefaults.shared.askBeforeDeletingAPost = isOn
|
||||
}
|
||||
case let .design(designSetting):
|
||||
switch designSetting {
|
||||
case .showAnimations:
|
||||
viewModel.playAnimations = isOn
|
||||
}
|
||||
}
|
||||
|
||||
delegate?.save(self, setting: self.setting, viewModel: viewModel)
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import MastodonLocalization
|
||||
import MastodonUI
|
||||
import SwiftUI
|
||||
|
||||
class LanguagePickerViewController: UIHostingController<LanguagePicker> {
|
||||
private let onLanguageSelected: OnLanguageSelected
|
||||
|
||||
init(onLanguageSelected: @escaping OnLanguageSelected) {
|
||||
self.onLanguageSelected = onLanguageSelected
|
||||
super.init(rootView: LanguagePicker(selectedLanguage: UserDefaults.shared.defaultPostLanguage, onSelect:self.onLanguageSelected))
|
||||
title = L10n.Scene.Settings.General.Language.defaultPostLanguage
|
||||
}
|
||||
|
||||
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import CoreDataStack
|
|||
import MastodonSDK
|
||||
import Combine
|
||||
import MetaTextKit
|
||||
import MastodonUI
|
||||
|
||||
protocol SettingsCoordinatorDelegate: AnyObject {
|
||||
func logout(_ settingsCoordinator: SettingsCoordinator)
|
||||
|
@ -57,9 +58,10 @@ extension SettingsCoordinator: SettingsViewControllerDelegate {
|
|||
func didSelect(_ viewController: UIViewController, entry: SettingsEntry) {
|
||||
switch entry {
|
||||
case .general:
|
||||
let generalSettingsViewController = GeneralSettingsViewController(setting: setting)
|
||||
|
||||
let generalSettingsViewController = GeneralSettingsViewController(appContext: appContext, setting: setting)
|
||||
generalSettingsViewController.delegate = self
|
||||
|
||||
|
||||
navigationController.pushViewController(generalSettingsViewController, animated: true)
|
||||
case .notifications:
|
||||
|
||||
|
@ -144,6 +146,11 @@ extension SettingsCoordinator: GeneralSettingsViewControllerDelegate {
|
|||
UserDefaults.shared.preferredStaticAvatar = viewModel.playAnimations == false
|
||||
UserDefaults.shared.preferredUsingDefaultBrowser = viewModel.selectedOpenLinks == .browser
|
||||
}
|
||||
|
||||
func showLanguagePicker(_ viewModel: GeneralSettingsViewModel, onLanguageSelected: @escaping OnLanguageSelected) {
|
||||
let viewController = LanguagePickerViewController(onLanguageSelected: onLanguageSelected)
|
||||
navigationController.pushViewController(viewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - NotificationSettingsViewControllerDelegate
|
||||
|
|
|
@ -47,7 +47,7 @@ extension UserTableViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextProvider {
|
||||
extension UserTableViewCellDelegate where Self: ViewControllerWithDependencies & AuthContextProvider {
|
||||
func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for account: Mastodon.Entity.Account, me: Mastodon.Entity.Account?) {
|
||||
Task {
|
||||
await MainActor.run { view.setButtonState(.loading) }
|
||||
|
|
|
@ -110,7 +110,7 @@ final class SuggestionAccountViewModel: NSObject {
|
|||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func followAllSuggestedAccounts(_ dependency: NeedsDependency & AuthContextProvider, presentedOn: UIViewController?, completion: (() -> Void)? = nil) {
|
||||
func followAllSuggestedAccounts(_ dependency: ViewControllerWithDependencies & AuthContextProvider, presentedOn: UIViewController?, completion: (() -> Void)? = nil) {
|
||||
|
||||
let tmpAccounts = accounts.compactMap { $0.account }
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension UserDefaults {
|
||||
|
||||
@objc public dynamic var askBeforePostingWithoutAltText: Bool {
|
||||
get {
|
||||
return object(forKey: #function) as? Bool ?? true
|
||||
}
|
||||
set { self[#function] = newValue }
|
||||
}
|
||||
|
||||
@objc public dynamic var askBeforeUnfollowingSomeone: Bool {
|
||||
get {
|
||||
return object(forKey: #function) as? Bool ?? true
|
||||
}
|
||||
set { self[#function] = newValue }
|
||||
}
|
||||
|
||||
@objc public dynamic var askBeforeBoostingAPost: Bool {
|
||||
get {
|
||||
return object(forKey: #function) as? Bool ?? true
|
||||
}
|
||||
set { self[#function] = newValue }
|
||||
}
|
||||
|
||||
@objc public dynamic var askBeforeDeletingAPost: Bool {
|
||||
get {
|
||||
return object(forKey: #function) as? Bool ?? true
|
||||
}
|
||||
set { self[#function] = newValue }
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension UserDefaults {
|
||||
|
||||
@objc public dynamic var defaultPostLanguage: String {
|
||||
get {
|
||||
return object(forKey: #function) as? String ?? Locale.current.language.languageCode?.identifier ?? "en"
|
||||
}
|
||||
set { self[#function] = newValue }
|
||||
}
|
||||
|
||||
}
|
|
@ -10,4 +10,5 @@ import Foundation
|
|||
public enum AppError: Error {
|
||||
case badRequest
|
||||
case badAuthentication
|
||||
case unexpected
|
||||
}
|
||||
|
|
|
@ -20,6 +20,18 @@ public enum L10n {
|
|||
return L10n.tr("Localizable", "Common.Alerts.BlockDomain.Title", String(describing: p1), fallback: "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain and any of your followers from that domain will be removed.")
|
||||
}
|
||||
}
|
||||
public enum BoostAPost {
|
||||
/// Boost
|
||||
public static let boost = L10n.tr("Localizable", "Common.Alerts.BoostAPost.Boost", fallback: "Boost")
|
||||
/// Cancel
|
||||
public static let cancel = L10n.tr("Localizable", "Common.Alerts.BoostAPost.Cancel", fallback: "Cancel")
|
||||
/// Boost Post?
|
||||
public static let titleBoost = L10n.tr("Localizable", "Common.Alerts.BoostAPost.TitleBoost", fallback: "Boost Post?")
|
||||
/// Unboost Post?
|
||||
public static let titleUnboost = L10n.tr("Localizable", "Common.Alerts.BoostAPost.TitleUnboost", fallback: "Unboost Post?")
|
||||
/// Unboost
|
||||
public static let unboost = L10n.tr("Localizable", "Common.Alerts.BoostAPost.Unboost", fallback: "Unboost")
|
||||
}
|
||||
public enum CleanCache {
|
||||
/// Successfully cleaned %@ cache.
|
||||
public static func message(_ p1: Any) -> String {
|
||||
|
@ -46,6 +58,19 @@ public enum L10n {
|
|||
/// Edit Profile Error
|
||||
public static let title = L10n.tr("Localizable", "Common.Alerts.EditProfileFailure.Title", fallback: "Edit Profile Error")
|
||||
}
|
||||
public enum MediaMissingAltText {
|
||||
/// Cancel
|
||||
public static let cancel = L10n.tr("Localizable", "Common.Alerts.MediaMissingAltText.Cancel", fallback: "Cancel")
|
||||
/// %d of your images are missing alt text.
|
||||
/// Post Anyway?
|
||||
public static func message(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "Common.Alerts.MediaMissingAltText.Message", p1, fallback: "%d of your images are missing alt text.\nPost Anyway?")
|
||||
}
|
||||
/// Post
|
||||
public static let post = L10n.tr("Localizable", "Common.Alerts.MediaMissingAltText.Post", fallback: "Post")
|
||||
/// Media Missing Alt Text
|
||||
public static let title = L10n.tr("Localizable", "Common.Alerts.MediaMissingAltText.Title", fallback: "Media Missing Alt Text")
|
||||
}
|
||||
public enum PublishPostFailure {
|
||||
/// Failed to publish the post.
|
||||
/// Please check your internet connection.
|
||||
|
@ -89,6 +114,16 @@ public enum L10n {
|
|||
/// Note
|
||||
public static let title = L10n.tr("Localizable", "Common.Alerts.TranslationFailed.Title", fallback: "Note")
|
||||
}
|
||||
public enum UnfollowUser {
|
||||
/// Cancel
|
||||
public static let cancel = L10n.tr("Localizable", "Common.Alerts.UnfollowUser.Cancel", fallback: "Cancel")
|
||||
/// Unfollow %@
|
||||
public static func title(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Alerts.UnfollowUser.Title", String(describing: p1), fallback: "Unfollow %@")
|
||||
}
|
||||
/// Unfollow
|
||||
public static let unfollow = L10n.tr("Localizable", "Common.Alerts.UnfollowUser.Unfollow", fallback: "Unfollow")
|
||||
}
|
||||
public enum VoteFailure {
|
||||
/// The poll has ended
|
||||
public static let pollEnded = L10n.tr("Localizable", "Common.Alerts.VoteFailure.PollEnded", fallback: "The poll has ended")
|
||||
|
@ -1459,12 +1494,30 @@ public enum L10n {
|
|||
/// Use Device Appearance
|
||||
public static let system = L10n.tr("Localizable", "Scene.Settings.General.Appearance.System", fallback: "Use Device Appearance")
|
||||
}
|
||||
public enum AskBefore {
|
||||
/// Boosting a Post
|
||||
public static let boostingAPost = L10n.tr("Localizable", "Scene.Settings.General.AskBefore.BoostingAPost", fallback: "Boosting a Post")
|
||||
/// Deleting a Post
|
||||
public static let deletingAPost = L10n.tr("Localizable", "Scene.Settings.General.AskBefore.DeletingAPost", fallback: "Deleting a Post")
|
||||
/// Posting without Alt Text
|
||||
public static let postingWithoutAltText = L10n.tr("Localizable", "Scene.Settings.General.AskBefore.PostingWithoutAltText", fallback: "Posting without Alt Text")
|
||||
/// Ask before…
|
||||
public static let sectionTitle = L10n.tr("Localizable", "Scene.Settings.General.AskBefore.SectionTitle", fallback: "Ask before…")
|
||||
/// Unfollowing Someone
|
||||
public static let unfollowingSomeone = L10n.tr("Localizable", "Scene.Settings.General.AskBefore.UnfollowingSomeone", fallback: "Unfollowing Someone")
|
||||
}
|
||||
public enum Design {
|
||||
/// Design
|
||||
public static let sectionTitle = L10n.tr("Localizable", "Scene.Settings.General.Design.SectionTitle", fallback: "Design")
|
||||
/// Play Animated Avatars and Emoji
|
||||
public static let showAnimations = L10n.tr("Localizable", "Scene.Settings.General.Design.ShowAnimations", fallback: "Play Animated Avatars and Emoji")
|
||||
}
|
||||
public enum Language {
|
||||
/// Default Post Language
|
||||
public static let defaultPostLanguage = L10n.tr("Localizable", "Scene.Settings.General.Language.DefaultPostLanguage", fallback: "Default Post Language")
|
||||
/// Language
|
||||
public static let sectionTitle = L10n.tr("Localizable", "Scene.Settings.General.Language.SectionTitle", fallback: "Language")
|
||||
}
|
||||
public enum Links {
|
||||
/// Open in Browser
|
||||
public static let openInBrowser = L10n.tr("Localizable", "Scene.Settings.General.Links.OpenInBrowser", fallback: "Open in Browser")
|
||||
|
|
|
@ -25,6 +25,18 @@ Please check your internet connection.";
|
|||
"Common.Alerts.TranslationFailed.Title" = "Note";
|
||||
"Common.Alerts.VoteFailure.PollEnded" = "The poll has ended";
|
||||
"Common.Alerts.VoteFailure.Title" = "Vote Failure";
|
||||
"Common.Alerts.MediaMissingAltText.Title" = "Media Missing Alt Text";
|
||||
"Common.Alerts.MediaMissingAltText.Message" = "%d of your images are missing alt text.\nPost Anyway?";
|
||||
"Common.Alerts.MediaMissingAltText.Cancel" = "Cancel";
|
||||
"Common.Alerts.MediaMissingAltText.Post" = "Post";
|
||||
"Common.Alerts.BoostAPost.TitleBoost" = "Boost Post?";
|
||||
"Common.Alerts.BoostAPost.TitleUnboost" = "Unboost Post?";
|
||||
"Common.Alerts.BoostAPost.Cancel" = "Cancel";
|
||||
"Common.Alerts.BoostAPost.Boost" = "Boost";
|
||||
"Common.Alerts.BoostAPost.Unboost" = "Unboost";
|
||||
"Common.Alerts.UnfollowUser.Title" = "Unfollow %@";
|
||||
"Common.Alerts.UnfollowUser.Cancel" = "Cancel";
|
||||
"Common.Alerts.UnfollowUser.Unfollow" = "Unfollow";
|
||||
"Common.Controls.Actions.Add" = "Add";
|
||||
"Common.Controls.Actions.Back" = "Back";
|
||||
"Common.Controls.Actions.BlockDomain" = "Block %@";
|
||||
|
@ -503,8 +515,15 @@ uploaded to Mastodon.";
|
|||
"Scene.Settings.General.Appearance.Light" = "Light";
|
||||
"Scene.Settings.General.Appearance.SectionTitle" = "Appearance";
|
||||
"Scene.Settings.General.Appearance.System" = "Use Device Appearance";
|
||||
"Scene.Settings.General.AskBefore.SectionTitle" = "Ask before…";
|
||||
"Scene.Settings.General.AskBefore.PostingWithoutAltText" = "Posting without Alt Text";
|
||||
"Scene.Settings.General.AskBefore.UnfollowingSomeone" = "Unfollowing Someone";
|
||||
"Scene.Settings.General.AskBefore.BoostingAPost" = "Boosting a Post";
|
||||
"Scene.Settings.General.AskBefore.DeletingAPost" = "Deleting a Post";
|
||||
"Scene.Settings.General.Design.SectionTitle" = "Design";
|
||||
"Scene.Settings.General.Design.ShowAnimations" = "Play Animated Avatars and Emoji";
|
||||
"Scene.Settings.General.Language.SectionTitle" = "Language";
|
||||
"Scene.Settings.General.Language.DefaultPostLanguage" = "Default Post Language";
|
||||
"Scene.Settings.General.Links.OpenInBrowser" = "Open in Browser";
|
||||
"Scene.Settings.General.Links.OpenInMastodon" = "Open in Mastodon";
|
||||
"Scene.Settings.General.Links.SectionTitle" = "Links";
|
||||
|
@ -568,4 +587,4 @@ uploaded to Mastodon.";
|
|||
"Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts.";
|
||||
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
||||
"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
||||
|
|
|
@ -45,7 +45,7 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
|||
public let authContext: AuthContext
|
||||
public let input: Input
|
||||
public let sizeLimit: SizeLimit
|
||||
@Published var caption = ""
|
||||
@Published public internal(set) var caption = ""
|
||||
@Published public private(set) var isCaptionEditable = true
|
||||
let isEditing: Bool
|
||||
|
||||
|
|
|
@ -185,7 +185,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
|
|||
|
||||
let recentLanguages = context.settingService.currentSetting.value?.recentLanguages ?? []
|
||||
self.recentLanguages = recentLanguages
|
||||
self.language = recentLanguages.first ?? Locale.current.language.languageCode?.identifier ?? "en"
|
||||
self.language = UserDefaults.shared.defaultPostLanguage
|
||||
super.init()
|
||||
// end init
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ extension ComposeContentToolbarView {
|
|||
@Published var isAttachmentButtonEnabled = false
|
||||
@Published var isPollButtonEnabled = false
|
||||
|
||||
@Published var language = Locale.current.language.languageCode?.identifier ?? "en"
|
||||
@Published var language = UserDefaults.shared.defaultPostLanguage
|
||||
@Published var recentLanguages: [String] = []
|
||||
|
||||
@Published public var maxTextInputLimit = 500
|
||||
|
|
|
@ -142,7 +142,7 @@ struct ComposeContentToolbarView: View {
|
|||
}
|
||||
.frame(width: 48, height: 48)
|
||||
.popover(isPresented: $showingLanguagePicker) {
|
||||
let picker = LanguagePicker { newLanguage in
|
||||
let picker = LanguagePickerNavigationView(selectedLanguage: viewModel.language) { newLanguage in
|
||||
viewModel.language = newLanguage
|
||||
didChangeLanguage = true
|
||||
showingLanguagePicker = false
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
import Foundation
|
||||
|
||||
// Consider replacing this with Locale.Language when dropping iOS 15
|
||||
struct Language: Identifiable {
|
||||
let endonym: String
|
||||
let exonym: String
|
||||
let id: String
|
||||
let localeId: String?
|
||||
public struct Language: Identifiable {
|
||||
public let endonym: String
|
||||
public let exonym: String
|
||||
public let id: String
|
||||
public let localeId: String?
|
||||
|
||||
init(endonym: String, exonym: String, id: String, localeId: String?) {
|
||||
self.endonym = endonym
|
||||
|
|
|
@ -2,15 +2,53 @@
|
|||
|
||||
import MastodonLocalization
|
||||
import SwiftUI
|
||||
import MastodonAsset
|
||||
|
||||
struct LanguagePicker: View {
|
||||
let onSelect: (String) -> Void
|
||||
public typealias OnLanguageSelected = (String) -> Void
|
||||
|
||||
struct LanguagePickerNavigationView: View {
|
||||
|
||||
public init(selectedLanguage: String, onSelect: @escaping OnLanguageSelected) {
|
||||
self._selectedLanguage = State(initialValue: selectedLanguage)
|
||||
self.onSelect = onSelect
|
||||
}
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var selectedLanguage: String
|
||||
private let onSelect: OnLanguageSelected
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
LanguagePicker(selectedLanguage: selectedLanguage) { onSelect($0) }
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(L10n.Common.Controls.Actions.cancel) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.Scene.Compose.Language.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}.navigationViewStyle(.stack)
|
||||
}
|
||||
}
|
||||
|
||||
public struct LanguagePicker: View {
|
||||
|
||||
public init(selectedLanguage: String, onSelect: @escaping OnLanguageSelected) {
|
||||
self._selectedLanguage = State(initialValue: selectedLanguage)
|
||||
self.onSelect = onSelect
|
||||
}
|
||||
|
||||
@State private var selectedLanguage: String
|
||||
private let onSelect: OnLanguageSelected
|
||||
|
||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||
|
||||
@State private var query = ""
|
||||
@State private var languages: [Language] = {
|
||||
@State private var languages: [Language] = availableLanguages()
|
||||
|
||||
public static func availableLanguages() -> [Language] {
|
||||
let locales = Locale.availableIdentifiers.map(Locale.init(identifier:))
|
||||
var languages: [String: Language] = [:]
|
||||
for locale in locales {
|
||||
|
@ -23,10 +61,10 @@ struct LanguagePicker: View {
|
|||
}
|
||||
}
|
||||
return languages.values.sorted(using: KeyPathComparator(\.id))
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ScrollViewReader { proxy in
|
||||
let filteredLanguages = query.isEmpty ? languages : languages.filter { $0.contains(query) }
|
||||
List(filteredLanguages) { lang in
|
||||
let endonym = Text(lang.endonym)
|
||||
|
@ -36,31 +74,41 @@ struct LanguagePicker: View {
|
|||
}
|
||||
return Text("")
|
||||
}()
|
||||
Button(action: { onSelect(lang.id) }) {
|
||||
ViewThatFits(in: .horizontal) {
|
||||
HStack(spacing: 0) { endonym; Text(" "); exonym }
|
||||
VStack(alignment: .leading) { endonym; exonym }
|
||||
Button(action: {
|
||||
selectedLanguage = lang.id
|
||||
onSelect(lang.id)
|
||||
}) {
|
||||
HStack {
|
||||
ViewThatFits(in: .horizontal) {
|
||||
HStack(spacing: 0) { endonym; Text(" "); exonym }
|
||||
VStack(alignment: .leading) { endonym; exonym }
|
||||
}
|
||||
if lang.id == selectedLanguage {
|
||||
Spacer()
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundStyle(Asset.Colors.Brand.blurple.swiftUIColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tint(.primary)
|
||||
.accessibilityLabel(Text(lang.label))
|
||||
}.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(L10n.Common.Controls.Actions.cancel) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.id(lang.id)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.searchable(text: $query, placement: .navigationBarDrawer(displayMode: .always))
|
||||
.navigationTitle(L10n.Scene.Compose.Language.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}.navigationViewStyle(.stack)
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { // when scrolling to quickly it'll overlap with other drawcycles and mess up the position :-(
|
||||
if let selectedIndex = filteredLanguages.first(where: { $0.id == selectedLanguage }) {
|
||||
proxy.scrollTo(selectedIndex.id, anchor: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SwiftUIView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LanguagePicker(onSelect: { _ in })
|
||||
LanguagePicker(selectedLanguage: "en", onSelect: { _ in })
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue