Skip to content

Commit

Permalink
Implement "Default Post Language" and "Ask Before"-Alerts (#1240)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
kimar authored Feb 28, 2024
1 parent d3ffa37 commit 2e7054c
Show file tree
Hide file tree
Showing 26 changed files with 609 additions and 105 deletions.
29 changes: 29 additions & 0 deletions Localization/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions Mastodon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -1947,6 +1957,7 @@
D8F917042A4B0657008A5370 /* General Settings */ = {
isa = PBXGroup;
children = (
2A631AE62B8C9F5900FE0778 /* Language */,
D8318A832A4468A800C0FB73 /* GeneralSettingsViewController.swift */,
D8F917052A4B0791008A5370 /* GeneralSettings.swift */,
D8F917102A4C6B40008A5370 /* GeneralSettingToggleTableViewCell.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down
72 changes: 57 additions & 15 deletions Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 response = try await dependency.context.apiService.toggleFollow(
account: account,
authenticationBox: dependency.authContext.mastodonAuthenticationBox
).value

dependency.context.authenticationService.fetchFollowingAndBlockedAsync()

NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [
UserInfoKey.relationship: response
])
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

dependency.context.authenticationService.fetchFollowingAndBlockedAsync()


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()
}
}
}
}

}
Expand Down
31 changes: 31 additions & 0 deletions Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
51 changes: 30 additions & 21 deletions Mastodon/Protocol/Provider/DataSourceFacade+Status.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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
)
}
}
}
2 changes: 1 addition & 1 deletion Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
55 changes: 50 additions & 5 deletions Mastodon/Scene/Compose/ComposeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -259,7 +305,6 @@ extension ComposeViewController {
}

dismiss(animated: true, completion: nil)

}
}

Expand Down
Loading

0 comments on commit 2e7054c

Please sign in to comment.