Share post as image close #885

This commit is contained in:
Thomas Ricouard 2023-02-19 15:29:07 +01:00
parent dd2ebe5506
commit ccc504fc6f
25 changed files with 257 additions and 56 deletions

View file

@ -7,6 +7,8 @@ import Lists
import Status
import SwiftUI
import Timeline
import LinkPresentation
import Models
@MainActor
extension View {
@ -44,7 +46,7 @@ extension View {
}
}
}
func withSheetDestinations(sheetDestinations: Binding<SheetDestinations?>) -> some View {
sheet(item: sheetDestinations) { destination in
switch destination {
@ -92,10 +94,12 @@ extension View {
case let .report(status):
ReportView(status: status)
.withEnvironments()
case let .shareImage(image, status):
ActivityView(image: image, status: status)
}
}
}
func withEnvironments() -> some View {
environmentObject(CurrentAccount.shared)
.environmentObject(UserPreferences.shared)
@ -106,3 +110,42 @@ extension View {
.environmentObject(AppAccountsManager.shared.currentClient)
}
}
struct ActivityView: UIViewControllerRepresentable {
let image: UIImage
let status: Status
class LinkDelegate: NSObject, UIActivityItemSource {
let image: UIImage
let status: Status
init(image: UIImage, status: Status) {
self.image = image
self.status = status
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let imageProvider = NSItemProvider(object: image)
let metadata = LPLinkMetadata()
metadata.imageProvider = imageProvider
metadata.title = status.reblog?.content.asRawText ?? status.content.asRawText
return metadata
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
image
}
func activityViewController(_ activityViewController: UIActivityViewController,
itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
nil
}
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
return UIActivityViewController(activityItems: [image, LinkDelegate(image: image, status: status)],
applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {}
}

View file

@ -372,6 +372,9 @@
"status.action.reply" = "Адказаць";
"status.action.section.your-post" = "Ваш допіс";
"status.action.share" = "Падзяліцца гэтым допісам";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Скасаваць закладку";
"status.action.unboost" = "Адмяніць павышэнне";
"status.action.unfavorite" = "Выдаліць з улюбенага";

View file

@ -382,6 +382,9 @@
"status.action.reply" = "Respon";
"status.action.section.your-post" = "La vostra publicació";
"status.action.share" = "Comparteix la publicació";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Elimina dels marcadors";
"status.action.unboost" = "Desfés l'impulsa";
"status.action.unfavorite" = "Desfés el preferit";

View file

@ -379,6 +379,9 @@
"status.action.reply" = "Antworten";
"status.action.section.your-post" = "Dein Beitrag";
"status.action.share" = "Diesen Beitrag teilen";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Lesezeichen entfernen";
"status.action.unboost" = "Boost entfernen";
"status.action.unfavorite" = "Favorit entfernen";

View file

@ -385,6 +385,9 @@
"status.action.reply" = "Reply";
"status.action.section.your-post" = "Your post";
"status.action.share" = "Share this post";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Unbookmark";
"status.action.unboost" = "Unboost";
"status.action.unfavorite" = "Unfavourite";

View file

@ -384,6 +384,9 @@
"status.action.reply" = "Reply";
"status.action.section.your-post" = "Your post";
"status.action.share" = "Share this post";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Unbookmark";
"status.action.unboost" = "Unboost";
"status.action.unfavorite" = "Unfavorite";

View file

@ -384,6 +384,9 @@
"status.action.reply" = "Responder";
"status.action.section.your-post" = "Tus publicaciones";
"status.action.share" = "Compartir esta publicación";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Quitar de marcadores";
"status.action.unboost" = "Deshacer Retoot";
"status.action.unfavorite" = "Eliminar de favoritos";

View file

@ -377,6 +377,9 @@
"status.action.reply" = "Erantzun";
"status.action.section.your-post" = "Zure bidalketa";
"status.action.share" = "Partekatu bidalketa";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Kendu laster-marka";
"status.action.unboost" = "Kendu bultzada";
"status.action.unfavorite" = "Kendu gogokoa";

View file

@ -379,6 +379,9 @@
"status.action.reply" = "Répondre";
"status.action.section.your-post" = "Votre publication";
"status.action.share" = "Partager cette publication";
"status.action.share-link" = "Partager le lien";
"status.action.share-image" = "Partager comme image";
"status.action.share-title" = "Partager";
"status.action.unbookmark" = "Démarquer";
"status.action.unboost" = "Annuler la promotion";
"status.action.unfavorite" = "Retirer des favoris";

View file

@ -384,6 +384,9 @@
"status.action.reply" = "Rispondi";
"status.action.section.your-post" = "I tuoi post";
"status.action.share" = "Condividi questo post";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Rimuovi il segnalibro";
"status.action.unboost" = "Rimuovi la condivisione";
"status.action.unfavorite" = "Rimuovi l'apprezzamento";

View file

@ -383,6 +383,9 @@
"status.action.reply" = "リプライ";
"status.action.section.your-post" = "あなたの投稿";
"status.action.share" = "投稿を共有する";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "ブックマークを外す";
"status.action.unboost" = "ブーストをやめる";
"status.action.unfavorite" = "お気に入りから外す";

View file

@ -385,6 +385,9 @@
"status.action.reply" = "댓글";
"status.action.section.your-post" = "내 글";
"status.action.share" = "공유";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "보관함에서 제거";
"status.action.unboost" = "부스트 취소";
"status.action.unfavorite" = "좋아요 취소";

View file

@ -383,6 +383,9 @@
"status.action.reply" = "Svar";
"status.action.section.your-post" = "Ditt innlegg";
"status.action.share" = "Del dette innlegget";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Fjern bokmerke";
"status.action.unboost" = "Opphev forsterkningen";
"status.action.unfavorite" = "Ikke favoritt";

View file

@ -377,6 +377,9 @@
"status.action.reply" = "Antwoord";
"status.action.section.your-post" = "Jouw post";
"status.action.share" = "Deel deze post";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Verwijder bladwijzer";
"status.action.unboost" = "Maak boost ongedaan";
"status.action.unfavorite" = "Verwijder favoriet";

View file

@ -379,6 +379,9 @@
"status.action.reply" = "Odpowiedz";
"status.action.section.your-post" = "Twój post";
"status.action.share" = "Udostępnij ten post";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Usuń zakładkę";
"status.action.unboost" = "Cofnij podbicie";
"status.action.unfavorite" = "Usuń z polubionych";

View file

@ -383,6 +383,9 @@
"status.action.reply" = "Responder";
"status.action.section.your-post" = "Sua postagem";
"status.action.share" = "Compartilhe esta postagem";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Remover dos salvos";
"status.action.unboost" = "Unboost";
"status.action.unfavorite" = "Desfavoritar";

View file

@ -379,6 +379,9 @@
"status.action.reply" = "Cevapla";
"status.action.section.your-post" = "Senin gönderin";
"status.action.share" = "Bu gönderiyi paylaş";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "Yer İmini Kaldır";
"status.action.unboost" = "Yükseltmeyi Kaldır";
"status.action.unfavorite" = "Favoriyi Kaldır";

View file

@ -384,6 +384,9 @@
"status.action.reply" = "回复";
"status.action.section.your-post" = "你的嘟文";
"status.action.share" = "分享嘟文";
"status.action.share-link" = "Share post link";
"status.action.share-image" = "Share post as image";
"status.action.share-title" = "Share";
"status.action.unbookmark" = "取消书签";
"status.action.unboost" = "取消转发";
"status.action.unfavorite" = "取消收藏";

View file

@ -1,8 +1,10 @@
import NukeUI
import Nuke
import Shimmer
import SwiftUI
public struct AvatarView: View {
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
@Environment(\.redactionReasons) private var reasons
@EnvironmentObject private var theme: Theme
@ -54,16 +56,24 @@ public struct AvatarView: View {
.fill(.gray)
.frame(width: size.size.width, height: size.size.height)
} else {
LazyImage(url: url) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fit)
} else {
placeholderView
if isInCaptureMode, let image = Nuke.ImagePipeline.shared.cache.cachedImage(for: .init(url: url))?.image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: size.size.width, height: size.size.height)
} else {
LazyImage(url: url) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fit)
} else {
placeholderView
}
}
.animation(nil)
.frame(width: size.size.width, height: size.size.height)
}
.frame(width: size.size.width, height: size.size.height)
}
}
.clipShape(clipShape)

View file

@ -13,6 +13,10 @@ private struct IsCompact: EnvironmentKey {
static let defaultValue: Bool = false
}
private struct IsInCaptureMode: EnvironmentKey {
static let defaultValue: Bool = false
}
public extension EnvironmentValues {
var isSecondaryColumn: Bool {
get { self[SecondaryColumnKey.self] }
@ -28,4 +32,9 @@ public extension EnvironmentValues {
get { self[IsCompact.self] }
set { self[IsCompact.self] = newValue }
}
var isInCaptureMode: Bool {
get { self[IsInCaptureMode.self] }
set { self[IsInCaptureMode.self] = newValue }
}
}

View file

@ -34,6 +34,7 @@ public enum SheetDestinations: Identifiable {
case settings
case accountPushNotficationsSettings
case report(status: Status)
case shareImage(image: UIImage, status: Status)
public var id: String {
switch self {
@ -52,6 +53,8 @@ public enum SheetDestinations: Identifiable {
return "statusEditHistory"
case .report:
return "report"
case .shareImage:
return "shareImage"
}
}
}

View file

@ -1,12 +1,15 @@
import DesignSystem
import Models
import Nuke
import NukeUI
import Shimmer
import SwiftUI
public struct StatusRowCardView: View {
@EnvironmentObject private var theme: Theme
@Environment(\.openURL) private var openURL
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
@EnvironmentObject private var theme: Theme
let card: Card
public init(card: Card) {
@ -16,8 +19,10 @@ public struct StatusRowCardView: View {
public var body: some View {
if let title = card.title, let url = URL(string: card.url) {
VStack(alignment: .leading) {
if let imageURL = card.image {
if let imageURL = card.image, !isInCaptureMode {
GeometryReader { proxy in
let processors: [ImageProcessing] = [.resize(size: .init(width: proxy.frame(in: .local).width,
height: 200))]
LazyImage(url: imageURL) { state in
if let image = state.image {
image
@ -32,6 +37,7 @@ public struct StatusRowCardView: View {
.frame(height: 200)
}
}
.processors(processors)
}
.frame(height: 200)
}

View file

@ -1,14 +1,19 @@
import Env
import Foundation
import SwiftUI
import DesignSystem
import Network
struct StatusRowContextMenu: View {
@Environment(\.displayScale) var displayScale
@EnvironmentObject private var sceneDelegate: SceneDelegate
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var account: CurrentAccount
@EnvironmentObject private var currentInstance: CurrentInstance
@ObservedObject var viewModel: StatusRowViewModel
var body: some View {
if !viewModel.isRemote {
Button { Task {
@ -56,13 +61,45 @@ struct StatusRowContextMenu: View {
Divider()
if let urlString = viewModel.status.reblog?.url ?? viewModel.status.url,
let url = URL(string: urlString)
{
ShareLink(item: url,
subject: Text(viewModel.status.reblog?.account.safeDisplayName ?? viewModel.status.account.safeDisplayName),
message: Text(viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText)) {
Label("status.action.share", systemImage: "square.and.arrow.up")
Menu("status.action.share-title") {
if let urlString = viewModel.status.reblog?.url ?? viewModel.status.url,
let url = URL(string: urlString)
{
ShareLink(item: url,
subject: Text(viewModel.status.reblog?.account.safeDisplayName ?? viewModel.status.account.safeDisplayName),
message: Text(viewModel.status.reblog?.content.asRawText ?? viewModel.status.content.asRawText)) {
Label("status.action.share", systemImage: "square.and.arrow.up")
}
ShareLink(item: url) {
Label("status.action.share-link", systemImage: "link")
}
Button {
let view = HStack {
StatusRowView(viewModel: viewModel)
.padding(16)
}
.environment(\.isInCaptureMode, true)
.environmentObject(Theme.shared)
.environmentObject(preferences)
.environmentObject(account)
.environmentObject(currentInstance)
.environmentObject(SceneDelegate())
.environmentObject(QuickLook())
.environmentObject(viewModel.client)
.preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light)
.background(Theme.shared.primaryBackgroundColor)
.cornerRadius(4)
.frame(width: sceneDelegate.windowWidth)
let renderer = ImageRenderer(content: view)
renderer.scale = displayScale
if let image = renderer.uiImage {
viewModel.routerPath.presentedSheet = .shareImage(image: image, status: viewModel.status)
}
} label: {
Label("status.action.share-image", systemImage: "photo")
}
}
}
@ -144,3 +181,13 @@ struct StatusRowContextMenu: View {
}
}
}
struct ActivityView: UIViewControllerRepresentable {
let image: Image
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
return UIActivityViewController(activityItems: [image], applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {}
}

View file

@ -3,6 +3,7 @@ import Models
import SwiftUI
struct StatusRowHeaderView: View {
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
@EnvironmentObject private var theme: Theme
let status: AnyStatus
@ -17,8 +18,10 @@ struct StatusRowHeaderView: View {
}
.buttonStyle(.plain)
Spacer()
threadIcon
contextMenuButton
if !isInCaptureMode {
threadIcon
contextMenuButton
}
}
.accessibilityElement()
.accessibilityLabel(Text("\(status.account.displayName)"))

View file

@ -7,8 +7,9 @@ import SwiftUI
public struct StatusRowMediaPreviewView: View {
@Environment(\.openURL) private var openURL
@Environment(\.isSecondaryColumn) private var isSecondaryColumn
@Environment(\.isSecondaryColumn) private var isSecondaryColumn: Bool
@Environment(\.extraLeadingInset) private var extraLeadingInset: CGFloat
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
@EnvironmentObject var sceneDelegate: SceneDelegate
@EnvironmentObject private var preferences: UserPreferences
@ -150,24 +151,36 @@ public struct StatusRowMediaPreviewView: View {
let size: CGSize = size(for: attachment) ?? .init(width: imageMaxHeight, height: imageMaxHeight)
let newSize = imageSize(from: size,
newWidth: availableWidth - appLayoutWidth)
let processors: [ImageProcessing] = [.resize(size: .init(width: newSize.width, height: newSize.height))]
switch attachment.supportedType {
case .image:
LazyImage(url: attachment.url) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: newSize.width, height: newSize.height)
.clipped()
.cornerRadius(4)
} else {
RoundedRectangle(cornerRadius: 4)
.fill(Color.gray)
.frame(width: newSize.width, height: newSize.height)
if isInCaptureMode,
let image = Nuke.ImagePipeline.shared.cache.cachedImage(for: .init(url: attachment.url,
processors: processors))?.image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: newSize.width, height: newSize.height)
.clipped()
.cornerRadius(4)
} else {
LazyImage(url: attachment.url) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: newSize.width, height: newSize.height)
.clipped()
.cornerRadius(4)
} else {
RoundedRectangle(cornerRadius: 4)
.fill(Color.gray)
.frame(width: newSize.width, height: newSize.height)
}
}
.processors([.resize(size: .init(width: newSize.width, height: newSize.height))])
.frame(width: newSize.width, height: newSize.height)
}
.processors([.resize(size: .init(width: newSize.width, height: newSize.height))])
.frame(width: newSize.width, height: newSize.height)
case .gifv, .video, .audio:
if let url = attachment.url {
@ -177,10 +190,10 @@ public struct StatusRowMediaPreviewView: View {
case .none:
EmptyView()
}
if sensitive {
if !isInCaptureMode, sensitive {
cornerSensitiveButton
}
if let alt = attachment.description, !alt.isEmpty, !isNotifications, preferences.showAltTextForMedia {
if !isInCaptureMode, let alt = attachment.description, !alt.isEmpty, !isNotifications, preferences.showAltTextForMedia {
Group {
Button {
altTextDisplayed = alt
@ -207,28 +220,44 @@ public struct StatusRowMediaPreviewView: View {
switch type {
case .image:
let width = isNotifications ? imageMaxHeight : proxy.frame(in: .local).width
let processors: [ImageProcessing] = [.resize(size: .init(width: width, height: imageMaxHeight))]
ZStack(alignment: .bottomTrailing) {
LazyImage(url: attachment.previewUrl ?? attachment.url) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: width)
.frame(maxHeight: imageMaxHeight)
.clipped()
.cornerRadius(4)
} else if state.isLoading {
RoundedRectangle(cornerRadius: 4)
.fill(Color.gray)
.frame(maxHeight: imageMaxHeight)
.frame(maxWidth: width)
if isInCaptureMode,
let image = Nuke.ImagePipeline.shared.cache.cachedImage(for: .init(url: attachment.previewUrl ?? attachment.url, processors: processors))?.image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: width)
.frame(maxHeight: imageMaxHeight)
.clipped()
.cornerRadius(4)
} else {
LazyImage(url: attachment.previewUrl ?? attachment.url) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: width)
.frame(maxHeight: imageMaxHeight)
.clipped()
.cornerRadius(4)
} else if state.isLoading {
RoundedRectangle(cornerRadius: 4)
.fill(Color.gray)
.frame(maxHeight: imageMaxHeight)
.frame(maxWidth: width)
}
}
.processors(processors)
}
.processors([.resize(size: .init(width: width, height: imageMaxHeight))])
if sensitive {
if sensitive, !isInCaptureMode {
cornerSensitiveButton
}
if let alt = attachment.description, !alt.isEmpty, !isNotifications, preferences.showAltTextForMedia {
if !isInCaptureMode,
let alt = attachment.description,
!alt.isEmpty,
!isNotifications,
preferences.showAltTextForMedia {
Button {
altTextDisplayed = alt
isAltAlertDisplayed = true