1
0
mirror of https://github.com/mastodon/mastodon-ios.git synced 2025-01-26 15:24:54 +01:00

Merge branch 'release/0.8.6' into main

This commit is contained in:
CMK 2021-07-06 20:10:40 +08:00
commit 6952fd2148
13 changed files with 172 additions and 61 deletions

View File

@ -70,15 +70,16 @@
"cancel": "Cancel", "cancel": "Cancel",
"discard": "Discard", "discard": "Discard",
"try_again": "Try Again", "try_again": "Try Again",
"take_photo": "Take photo", "take_photo": "Take Photo",
"save_photo": "Save photo", "save_photo": "Save Photo",
"copy_photo": "Copy Photo",
"sign_in": "Sign In", "sign_in": "Sign In",
"sign_up": "Sign Up", "sign_up": "Sign Up",
"see_more": "See More", "see_more": "See More",
"preview": "Preview", "preview": "Preview",
"share": "Share", "share": "Share",
"share_user": "Share %s", "share_user": "Share %s",
"share_post": "Share post", "share_post": "Share Post",
"open_in_safari": "Open in Safari", "open_in_safari": "Open in Safari",
"find_people": "Find people to follow", "find_people": "Find people to follow",
"manually_search": "Manually search instead", "manually_search": "Manually search instead",

View File

@ -3894,7 +3894,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
@ -3902,7 +3902,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.8.5; MARKETING_VERSION = 0.8.6;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -3921,7 +3921,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
@ -3929,7 +3929,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.8.5; MARKETING_VERSION = 0.8.6;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -4249,7 +4249,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
@ -4257,7 +4257,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.8.5; MARKETING_VERSION = 0.8.6;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -4363,7 +4363,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -4371,7 +4371,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 0.8.5; MARKETING_VERSION = 0.8.6;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -4482,7 +4482,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
@ -4490,7 +4490,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.8.5; MARKETING_VERSION = 0.8.6;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -4596,7 +4596,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -4604,7 +4604,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 0.8.5; MARKETING_VERSION = 0.8.6;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -4650,7 +4650,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -4658,7 +4658,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 0.8.5; MARKETING_VERSION = 0.8.6;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@ -4673,7 +4673,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 30; CURRENT_PROJECT_VERSION = 31;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -4681,7 +4681,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 0.8.5; MARKETING_VERSION = 0.8.6;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;

View File

@ -12,7 +12,7 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>20</integer> <integer>21</integer>
</dict> </dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key> <key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -37,7 +37,7 @@
<key>NotificationService.xcscheme_^#shared#^_</key> <key>NotificationService.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>21</integer> <integer>20</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -1005,7 +1005,8 @@ extension StatusSection {
private static func setupStatusMoreButtonMenu( private static func setupStatusMoreButtonMenu(
cell: StatusTableViewCell, cell: StatusTableViewCell,
dependency: NeedsDependency, dependency: NeedsDependency,
status: Status) { status: Status
) {
guard let userProvider = dependency as? UserProvider else { fatalError() } guard let userProvider = dependency as? UserProvider else { fatalError() }

View File

@ -110,6 +110,8 @@ internal enum L10n {
internal static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm") internal static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm")
/// Continue /// Continue
internal static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue") internal static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue")
/// Copy Photo
internal static let copyPhoto = L10n.tr("Localizable", "Common.Controls.Actions.CopyPhoto")
/// Delete /// Delete
internal static let delete = L10n.tr("Localizable", "Common.Controls.Actions.Delete") internal static let delete = L10n.tr("Localizable", "Common.Controls.Actions.Delete")
/// Discard /// Discard
@ -144,7 +146,7 @@ internal enum L10n {
} }
/// Save /// Save
internal static let save = L10n.tr("Localizable", "Common.Controls.Actions.Save") internal static let save = L10n.tr("Localizable", "Common.Controls.Actions.Save")
/// Save photo /// Save Photo
internal static let savePhoto = L10n.tr("Localizable", "Common.Controls.Actions.SavePhoto") internal static let savePhoto = L10n.tr("Localizable", "Common.Controls.Actions.SavePhoto")
/// See More /// See More
internal static let seeMore = L10n.tr("Localizable", "Common.Controls.Actions.SeeMore") internal static let seeMore = L10n.tr("Localizable", "Common.Controls.Actions.SeeMore")
@ -152,7 +154,7 @@ internal enum L10n {
internal static let settings = L10n.tr("Localizable", "Common.Controls.Actions.Settings") internal static let settings = L10n.tr("Localizable", "Common.Controls.Actions.Settings")
/// Share /// Share
internal static let share = L10n.tr("Localizable", "Common.Controls.Actions.Share") internal static let share = L10n.tr("Localizable", "Common.Controls.Actions.Share")
/// Share post /// Share Post
internal static let sharePost = L10n.tr("Localizable", "Common.Controls.Actions.SharePost") internal static let sharePost = L10n.tr("Localizable", "Common.Controls.Actions.SharePost")
/// Share %@ /// Share %@
internal static func shareUser(_ p1: Any) -> String { internal static func shareUser(_ p1: Any) -> String {
@ -164,7 +166,7 @@ internal enum L10n {
internal static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp") internal static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp")
/// Skip /// Skip
internal static let skip = L10n.tr("Localizable", "Common.Controls.Actions.Skip") internal static let skip = L10n.tr("Localizable", "Common.Controls.Actions.Skip")
/// Take photo /// Take Photo
internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto") internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto")
/// Try Again /// Try Again
internal static let tryAgain = L10n.tr("Localizable", "Common.Controls.Actions.TryAgain") internal static let tryAgain = L10n.tr("Localizable", "Common.Controls.Actions.TryAgain")

View File

@ -189,6 +189,31 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
}) })
.store(in: &self.context.disposeBag) .store(in: &self.context.disposeBag)
} }
let copyPhotoAction = UIAction(
title: L10n.Common.Controls.Actions.copyPhoto,
image: UIImage(systemName: "doc.on.doc"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off
) { [weak self] _ in
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: copy photo", ((#file as NSString).lastPathComponent), #line, #function)
guard let self = self else { return }
self.attachment(of: status, index: i)
.setFailureType(to: Error.self)
.compactMap { attachment -> AnyPublisher<UIImage, Error>? in
guard let attachment = attachment, let url = URL(string: attachment.url) else { return nil }
return self.context.photoLibraryService.copyImage(url: url)
}
.switchToLatest()
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: copy photo fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
case .finished:
break
}
}, receiveValue: { _ in
// do nothing
})
.store(in: &self.context.disposeBag)
}
let shareAction = UIAction( let shareAction = UIAction(
title: L10n.Common.Controls.Actions.share, image: UIImage(systemName: "square.and.arrow.up")!, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off title: L10n.Common.Controls.Actions.share, image: UIImage(systemName: "square.and.arrow.up")!, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off
) { [weak self] _ in ) { [weak self] _ in
@ -210,7 +235,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
}) })
.store(in: &self.context.disposeBag) .store(in: &self.context.disposeBag)
} }
let children = [savePhotoAction, shareAction] let children = [savePhotoAction, copyPhotoAction, shareAction]
return UIMenu(title: "", image: nil, children: children) return UIMenu(title: "", image: nil, children: children)
} }
contextMenuConfiguration.indexPath = indexPath contextMenuConfiguration.indexPath = indexPath

View File

@ -30,6 +30,7 @@ Please check your internet connection.";
"Common.Controls.Actions.Cancel" = "Cancel"; "Common.Controls.Actions.Cancel" = "Cancel";
"Common.Controls.Actions.Confirm" = "Confirm"; "Common.Controls.Actions.Confirm" = "Confirm";
"Common.Controls.Actions.Continue" = "Continue"; "Common.Controls.Actions.Continue" = "Continue";
"Common.Controls.Actions.CopyPhoto" = "Copy Photo";
"Common.Controls.Actions.Delete" = "Delete"; "Common.Controls.Actions.Delete" = "Delete";
"Common.Controls.Actions.Discard" = "Discard"; "Common.Controls.Actions.Discard" = "Discard";
"Common.Controls.Actions.Done" = "Done"; "Common.Controls.Actions.Done" = "Done";
@ -46,16 +47,16 @@ Please check your internet connection.";
"Common.Controls.Actions.Reply" = "Reply"; "Common.Controls.Actions.Reply" = "Reply";
"Common.Controls.Actions.ReportUser" = "Report %@"; "Common.Controls.Actions.ReportUser" = "Report %@";
"Common.Controls.Actions.Save" = "Save"; "Common.Controls.Actions.Save" = "Save";
"Common.Controls.Actions.SavePhoto" = "Save photo"; "Common.Controls.Actions.SavePhoto" = "Save Photo";
"Common.Controls.Actions.SeeMore" = "See More"; "Common.Controls.Actions.SeeMore" = "See More";
"Common.Controls.Actions.Settings" = "Settings"; "Common.Controls.Actions.Settings" = "Settings";
"Common.Controls.Actions.Share" = "Share"; "Common.Controls.Actions.Share" = "Share";
"Common.Controls.Actions.SharePost" = "Share post"; "Common.Controls.Actions.SharePost" = "Share Post";
"Common.Controls.Actions.ShareUser" = "Share %@"; "Common.Controls.Actions.ShareUser" = "Share %@";
"Common.Controls.Actions.SignIn" = "Sign In"; "Common.Controls.Actions.SignIn" = "Sign In";
"Common.Controls.Actions.SignUp" = "Sign Up"; "Common.Controls.Actions.SignUp" = "Sign Up";
"Common.Controls.Actions.Skip" = "Skip"; "Common.Controls.Actions.Skip" = "Skip";
"Common.Controls.Actions.TakePhoto" = "Take photo"; "Common.Controls.Actions.TakePhoto" = "Take Photo";
"Common.Controls.Actions.TryAgain" = "Try Again"; "Common.Controls.Actions.TryAgain" = "Try Again";
"Common.Controls.Actions.UnblockDomain" = "Unblock %@"; "Common.Controls.Actions.UnblockDomain" = "Unblock %@";
"Common.Controls.Friendship.Block" = "Block"; "Common.Controls.Friendship.Block" = "Block";

View File

@ -30,6 +30,7 @@ Please check your internet connection.";
"Common.Controls.Actions.Cancel" = "Cancel"; "Common.Controls.Actions.Cancel" = "Cancel";
"Common.Controls.Actions.Confirm" = "Confirm"; "Common.Controls.Actions.Confirm" = "Confirm";
"Common.Controls.Actions.Continue" = "Continue"; "Common.Controls.Actions.Continue" = "Continue";
"Common.Controls.Actions.CopyPhoto" = "Copy Photo";
"Common.Controls.Actions.Delete" = "Delete"; "Common.Controls.Actions.Delete" = "Delete";
"Common.Controls.Actions.Discard" = "Discard"; "Common.Controls.Actions.Discard" = "Discard";
"Common.Controls.Actions.Done" = "Done"; "Common.Controls.Actions.Done" = "Done";
@ -46,16 +47,16 @@ Please check your internet connection.";
"Common.Controls.Actions.Reply" = "Reply"; "Common.Controls.Actions.Reply" = "Reply";
"Common.Controls.Actions.ReportUser" = "Report %@"; "Common.Controls.Actions.ReportUser" = "Report %@";
"Common.Controls.Actions.Save" = "Save"; "Common.Controls.Actions.Save" = "Save";
"Common.Controls.Actions.SavePhoto" = "Save photo"; "Common.Controls.Actions.SavePhoto" = "Save Photo";
"Common.Controls.Actions.SeeMore" = "See More"; "Common.Controls.Actions.SeeMore" = "See More";
"Common.Controls.Actions.Settings" = "Settings"; "Common.Controls.Actions.Settings" = "Settings";
"Common.Controls.Actions.Share" = "Share"; "Common.Controls.Actions.Share" = "Share";
"Common.Controls.Actions.SharePost" = "Share post"; "Common.Controls.Actions.SharePost" = "Share Post";
"Common.Controls.Actions.ShareUser" = "Share %@"; "Common.Controls.Actions.ShareUser" = "Share %@";
"Common.Controls.Actions.SignIn" = "Sign In"; "Common.Controls.Actions.SignIn" = "Sign In";
"Common.Controls.Actions.SignUp" = "Sign Up"; "Common.Controls.Actions.SignUp" = "Sign Up";
"Common.Controls.Actions.Skip" = "Skip"; "Common.Controls.Actions.Skip" = "Skip";
"Common.Controls.Actions.TakePhoto" = "Take photo"; "Common.Controls.Actions.TakePhoto" = "Take Photo";
"Common.Controls.Actions.TryAgain" = "Try Again"; "Common.Controls.Actions.TryAgain" = "Try Again";
"Common.Controls.Actions.UnblockDomain" = "Unblock %@"; "Common.Controls.Actions.UnblockDomain" = "Unblock %@";
"Common.Controls.Friendship.Block" = "Block"; "Common.Controls.Friendship.Block" = "Block";

View File

@ -226,6 +226,24 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate {
case .local(let meta): case .local(let meta):
context.photoLibraryService.save(image: meta.image, withNotificationFeedback: true) context.photoLibraryService.save(image: meta.image, withNotificationFeedback: true)
} }
case .copyPhoto:
switch viewController.viewModel.item {
case .status(let meta):
context.photoLibraryService.copyImage(url: meta.url)
.sink { completion in
switch completion {
case .failure(let error):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: copy photo fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
case .finished:
break
}
} receiveValue: { _ in
// do nothing
}
.store(in: &context.disposeBag)
case .local(let meta):
context.photoLibraryService.copy(image: meta.image, withNotificationFeedback: true)
}
case .share: case .share:
let applicationActivities: [UIActivity] = [ let applicationActivities: [UIActivity] = [
SafariActivity(sceneCoordinator: self.coordinator) SafariActivity(sceneCoordinator: self.coordinator)
@ -236,6 +254,7 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate {
) )
activityViewController.popoverPresentationController?.sourceView = viewController.previewImageView.imageView activityViewController.popoverPresentationController?.sourceView = viewController.previewImageView.imageView
self.present(activityViewController, animated: true, completion: nil) self.present(activityViewController, animated: true, completion: nil)
} }
} }

View File

@ -133,6 +133,14 @@ extension MediaPreviewImageViewController: UIContextMenuInteractionDelegate {
guard let self = self else { return } guard let self = self else { return }
self.delegate?.mediaPreviewImageViewController(self, contextMenuActionPerform: .savePhoto) self.delegate?.mediaPreviewImageViewController(self, contextMenuActionPerform: .savePhoto)
} }
let copyAction = UIAction(
title: L10n.Common.Controls.Actions.copyPhoto, image: UIImage(systemName: "doc.on.doc")!, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off
) { [weak self] _ in
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: copy photo", ((#file as NSString).lastPathComponent), #line, #function)
guard let self = self else { return }
self.delegate?.mediaPreviewImageViewController(self, contextMenuActionPerform: .copyPhoto)
}
let shareAction = UIAction( let shareAction = UIAction(
title: L10n.Common.Controls.Actions.share, image: UIImage(systemName: "square.and.arrow.up")!, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off title: L10n.Common.Controls.Actions.share, image: UIImage(systemName: "square.and.arrow.up")!, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off
@ -145,6 +153,7 @@ extension MediaPreviewImageViewController: UIContextMenuInteractionDelegate {
let actionProvider: UIContextMenuActionProvider = { elements -> UIMenu? in let actionProvider: UIContextMenuActionProvider = { elements -> UIMenu? in
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [ return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
saveAction, saveAction,
copyAction,
shareAction shareAction
]) ])
} }
@ -162,6 +171,7 @@ extension MediaPreviewImageViewController: UIContextMenuInteractionDelegate {
extension MediaPreviewImageViewController { extension MediaPreviewImageViewController {
enum ContextMenuAction { enum ContextMenuAction {
case savePhoto case savePhoto
case copyPhoto
case share case share
} }
} }

View File

@ -7,10 +7,13 @@
import Foundation import Foundation
import UIKit import UIKit
import Combine
final class SawToothView: UIView { final class SawToothView: UIView {
static let widthUint = 8 static let widthUint = 8
var disposeBag = Set<AnyCancellable>()
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
_init() _init()
@ -22,7 +25,19 @@ final class SawToothView: UIView {
} }
func _init() { func _init() {
backgroundColor = Asset.Colors.Background.secondarySystemBackground.color setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackgroundColor(theme: theme)
}
.store(in: &disposeBag)
}
private func setupBackgroundColor(theme: Theme) {
backgroundColor = theme.secondarySystemBackgroundColor
setNeedsDisplay()
} }
override func draw(_ rect: CGRect) { override func draw(_ rect: CGRect) {
@ -37,7 +52,7 @@ final class SawToothView: UIView {
} }
bezierPath.addLine(to: CGPoint(x: 0, y: bottomY)) bezierPath.addLine(to: CGPoint(x: 0, y: bottomY))
bezierPath.close() bezierPath.close()
Asset.Colors.Background.systemBackground.color.setFill() ThemeService.shared.currentTheme.value.systemBackgroundColor.setFill()
bezierPath.fill() bezierPath.fill()
bezierPath.lineWidth = 0 bezierPath.lineWidth = 0
bezierPath.stroke() bezierPath.stroke()

View File

@ -16,13 +16,14 @@ class TimelineLoaderTableViewCell: UITableViewCell {
static let labelFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium)) static let labelFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium))
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
private var _disposeBag = Set<AnyCancellable>()
let stackView = UIStackView() let stackView = UIStackView()
let loadMoreButton: UIButton = { let loadMoreButton: UIButton = {
let button = HighlightDimmableButton() let button = HighlightDimmableButton()
button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont
button.backgroundColor = Asset.Colors.Background.systemBackground.color
button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal)
button.setTitle(L10n.Common.Controls.Timeline.Loader.loadMissingPosts, for: .normal) button.setTitle(L10n.Common.Controls.Timeline.Loader.loadMissingPosts, for: .normal)
button.setTitle("", for: .disabled) button.setTitle("", for: .disabled)
@ -114,6 +115,19 @@ class TimelineLoaderTableViewCell: UITableViewCell {
loadMoreButton.isHidden = true loadMoreButton.isHidden = true
loadMoreLabel.isHidden = true loadMoreLabel.isHidden = true
activityIndicatorView.isHidden = true activityIndicatorView.isHidden = true
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackgroundColor(theme: theme)
}
.store(in: &_disposeBag)
}
private func setupBackgroundColor(theme: Theme) {
loadMoreButton.backgroundColor = theme.systemBackgroundColor
} }
} }

View File

@ -9,7 +9,7 @@ import os.log
import UIKit import UIKit
import Combine import Combine
import Photos import Photos
import AlamofireImage import Nuke
final class PhotoLibraryService: NSObject { final class PhotoLibraryService: NSObject {
@ -26,39 +26,51 @@ extension PhotoLibraryService {
extension PhotoLibraryService { extension PhotoLibraryService {
func saveImage(url: URL) -> AnyPublisher<UIImage, Error> { func saveImage(url: URL) -> AnyPublisher<UIImage, Error> {
guard PHPhotoLibrary.authorizationStatus(for: .addOnly) != .denied else {
return Fail(error: PhotoLibraryError.noPermission).eraseToAnyPublisher()
}
return processImage(url: url)
.handleEvents(receiveOutput: { image in
self.save(image: image)
})
.eraseToAnyPublisher()
}
func copyImage(url: URL) -> AnyPublisher<UIImage, Error> {
return processImage(url: url)
.handleEvents(receiveOutput: { image in
UIPasteboard.general.image = image
})
.eraseToAnyPublisher()
}
func processImage(url: URL) -> AnyPublisher<UIImage, Error> {
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
let notificationFeedbackGenerator = UINotificationFeedbackGenerator() let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
return Future<UIImage, Error> { promise in return ImagePipeline.shared.imagePublisher(with: url)
guard PHPhotoLibrary.authorizationStatus(for: .addOnly) != .denied else { .handleEvents(receiveSubscription: { _ in
promise(.failure(PhotoLibraryError.noPermission)) impactFeedbackGenerator.impactOccurred()
return }, receiveOutput: { response in
} self.save(image: response.image)
}, receiveCompletion: { completion in
ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in switch completion {
guard let self = self else { return }
switch response.result {
case .failure(let error): case .failure(let error):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription)
promise(.failure(error))
case .success(let image): notificationFeedbackGenerator.notificationOccurred(.error)
case .finished:
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription)
self.save(image: image)
promise(.success(image)) notificationFeedbackGenerator.notificationOccurred(.success)
} }
}) })
} .map { response in
.handleEvents(receiveSubscription: { _ in return response.image
impactFeedbackGenerator.impactOccurred()
}, receiveCompletion: { completion in
switch completion {
case .failure:
notificationFeedbackGenerator.notificationOccurred(.error)
case .finished:
notificationFeedbackGenerator.notificationOccurred(.success)
} }
}) .mapError { error in error as Error }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func save(image: UIImage, withNotificationFeedback: Bool = false) { func save(image: UIImage, withNotificationFeedback: Bool = false) {
@ -75,6 +87,16 @@ extension PhotoLibraryService {
notificationFeedbackGenerator.notificationOccurred(.success) notificationFeedbackGenerator.notificationOccurred(.success)
} }
} }
func copy(image: UIImage, withNotificationFeedback: Bool = false) {
UIPasteboard.general.image = image
// assert no error
if withNotificationFeedback {
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
notificationFeedbackGenerator.notificationOccurred(.success)
}
}
@objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) { @objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
// TODO: notify banner // TODO: notify banner