diff --git a/Threaded.xcodeproj/project.pbxproj b/Threaded.xcodeproj/project.pbxproj index 2cdb6f3..bcdd681 100644 --- a/Threaded.xcodeproj/project.pbxproj +++ b/Threaded.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ B999DE5C2B76F8CB00509868 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B999DE5B2B76F8CB00509868 /* ContactsView.swift */; }; B999DE5E2B76F9D100509868 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = B999DE5D2B76F9D100509868 /* Message.swift */; }; B999DE602B76FB3E00509868 /* ContactRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B999DE5F2B76FB3E00509868 /* ContactRow.swift */; }; + B9A80DDA2C66DE1000DE3D88 /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A80DD92C66DE1000DE3D88 /* ReportStatusView.swift */; }; B9A8DABA2BB7364300A890CC /* PostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A8DAB92BB7364300A890CC /* PostsView.swift */; }; B9B469B02B9A275F00AD5585 /* FollowGoalWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469AF2B9A275F00AD5585 /* FollowGoalWidget.swift */; }; B9B469B22B9A6E8300AD5585 /* PrivacyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B469B12B9A6E8300AD5585 /* PrivacyView.swift */; }; @@ -226,6 +227,7 @@ B999DE5B2B76F8CB00509868 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = ""; }; B999DE5D2B76F9D100509868 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; B999DE5F2B76FB3E00509868 /* ContactRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRow.swift; sourceTree = ""; }; + B9A80DD92C66DE1000DE3D88 /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = ""; }; B9A8DAB92BB7364300A890CC /* PostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsView.swift; sourceTree = ""; }; B9B469AF2B9A275F00AD5585 /* FollowGoalWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowGoalWidget.swift; sourceTree = ""; }; B9B469B12B9A6E8300AD5585 /* PrivacyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyView.swift; sourceTree = ""; }; @@ -540,6 +542,7 @@ B93B677B2B433A6E000892E9 /* PostingView.swift */, B9F8FA152B5D3AC30044DAB4 /* SafariView.swift */, B9A8DAB92BB7364300A890CC /* PostsView.swift */, + B9A80DD92C66DE1000DE3D88 /* ReportStatusView.swift */, ); path = Views; sourceTree = ""; @@ -910,6 +913,7 @@ B9EBE8562B47256900FB594D /* PostAttachment.swift in Sources */, B9EBE8582B474FD600FB594D /* AppDelegate.swift in Sources */, B9FD18982C55108F00A74A71 /* EditProfileView.swift in Sources */, + B9A80DDA2C66DE1000DE3D88 /* ReportStatusView.swift in Sources */, B93B677C2B433A6E000892E9 /* PostingView.swift in Sources */, B97BCE262B3DE5A10044756D /* AccountView.swift in Sources */, B98627312B86F23500844245 /* LoggedAccounts.swift in Sources */, diff --git a/Threaded/Components/Post/CompactPostView.swift b/Threaded/Components/Post/CompactPostView.swift index fb3bac8..797acdb 100644 --- a/Threaded/Components/Post/CompactPostView.swift +++ b/Threaded/Components/Post/CompactPostView.swift @@ -36,6 +36,10 @@ struct CompactPostView: View { } } .withCovers(sheetDestination: $navigator.presentedCover) + .containerShape(Rectangle()) + .contextMenu { + PostMenu(status: status) + } .onAppear { do { preferences = try UserPreferences.loadAsCurrent() ?? UserPreferences.defaultPreferences @@ -136,27 +140,7 @@ struct CompactPostView: View { PostCardView(card: status.card!) } - if !status.mediaAttachments.isEmpty { - if status.mediaAttachments.count > 1 { - ScrollView(.horizontal, showsIndicators: false) { - HStack(alignment: .firstTextBaseline, spacing: 5) { - ForEach(status.mediaAttachments) { attachment in - PostAttachment(attachment: attachment, isFeatured: false, isImaging: imaging) - .blur(radius: status.sensitive ? 15.0 : 0) - .onTapGesture { - navigator.presentedCover = .media(attachments: status.mediaAttachments, selected: attachment) - } - } - } - } - .scrollClipDisabled() - } else { - PostAttachment(attachment: status.mediaAttachments.first!, isImaging: imaging) - .onTapGesture { - navigator.presentedCover = .media(attachments: status.mediaAttachments, selected: status.mediaAttachments[0]) - } - } - } + attachmnts // } if hasQuote && !quoted { @@ -183,7 +167,32 @@ struct CompactPostView: View { // } } } - + + @ViewBuilder + var attachmnts: some View { + if !status.mediaAttachments.isEmpty { + if status.mediaAttachments.count > 1 { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .firstTextBaseline, spacing: 5) { + ForEach(status.mediaAttachments) { attachment in + PostAttachment(attachment: attachment, isFeatured: false, isImaging: imaging) + .blur(radius: status.sensitive ? 15.0 : 0) + .onTapGesture { + navigator.presentedCover = .media(attachments: status.mediaAttachments, selected: attachment) + } + } + } + } + .scrollClipDisabled() + } else { + PostAttachment(attachment: status.mediaAttachments.first!, isImaging: imaging) + .onTapGesture { + navigator.presentedCover = .media(attachments: status.mediaAttachments, selected: status.mediaAttachments[0]) + } + } + } + } + var notices: some View { ZStack { if pinned { diff --git a/Threaded/Components/Post/PostMenu.swift b/Threaded/Components/Post/PostMenu.swift index 89d2bac..6bf5f47 100644 --- a/Threaded/Components/Post/PostMenu.swift +++ b/Threaded/Components/Post/PostMenu.swift @@ -22,80 +22,80 @@ struct PostMenu: View { } return false } - + var body: some View { - Menu { - if isOwner { - Button(role: .destructive) { - Task { - await deleteStatus() - } - } label: { - Label("status.menu.delete", systemImage: "trash") - } - - Button { - navigator.presentedSheet = .post(content: status.reblogAsAsStatus?.content.asRawText ?? status.content.asRawText, replyId: nil, editId: status.reblogAsAsStatus?.id ?? status.id) - } label: { - Label("status.menu.edit", systemImage: "pencil.and.scribble") - } - - Divider() - - Menu { - Button { - openURL(URL(string: AltClients.IvoryApp.createPost(status.reblogAsAsStatus?.content.asRawText ?? status.content.asRawText))!) - } label: { - Text(AltClients.IvoryApp.name) - } - - Button { - openURL(URL(string: AltClients.ThreadsApp.createPost(status.reblogAsAsStatus?.content.asRawText ?? status.content.asRawText))!) - } label: { - Text(AltClients.ThreadsApp.name) - } - - Button { - openURL(URL(string: AltClients.XApp.createPost(status.reblogAsAsStatus?.content.asRawText ?? status.content.asRawText))!) - } label: { - Text(AltClients.XApp.name) - } - } label: { - Label("status.cross-post.alts", systemImage: "shuffle") - } - - Divider() - } - - Menu { - ShareLink(item: URL(string: status.url ?? "https://joinmastodon.org/")!) { - Label("status.menu.share-link", systemImage: "square.and.arrow.up") - } - - Button { - Task { - createImage() - } - } label: { - Label("status.menu.share-image", systemImage: "photo") - } - - Divider() - - Button { - UIPasteboard.general.setValue(status.reblogAsAsStatus?.content.asRawText ?? status.content.asRawText, forPasteboardType: UTType.plainText.identifier) - } label: { - Label("status.menu.copy-text", systemImage: "list.clipboard") + if isOwner { + Button(role: .destructive) { + Task { + await deleteStatus() } } label: { - Label("status.menu.share", systemImage: "paperplane") + Label("status.menu.delete", systemImage: "trash") + } + + Button { + navigator.presentedSheet = .post(content: status.reblogAsAsStatus?.content.asRawText ?? status.content.asRawText, replyId: nil, editId: status.reblogAsAsStatus?.id ?? status.id) + } label: { + Label("status.menu.edit", systemImage: "pencil.and.scribble") + } + + Divider() + + Menu { + Button { + openURL(URL(string: AltClients.IvoryApp.createPost(status.reblogAsAsStatus?.content.asRawText ?? status.content.asRawText))!) + } label: { + Text(AltClients.IvoryApp.name) + } + + Button { + openURL(URL(string: AltClients.ThreadsApp.createPost(status.reblogAsAsStatus?.content.asRawText ?? status.content.asRawText))!) + } label: { + Text(AltClients.ThreadsApp.name) + } + + Button { + openURL(URL(string: AltClients.XApp.createPost(status.reblogAsAsStatus?.content.asRawText ?? status.content.asRawText))!) + } label: { + Text(AltClients.XApp.name) + } + } label: { + Label("status.cross-post.alts", systemImage: "shuffle") + } + + Divider() + } + + Menu { + ShareLink(item: URL(string: status.url ?? "https://joinmastodon.org/")!) { + Label("status.menu.share-link", systemImage: "square.and.arrow.up") + } + + Button { + Task { + createImage() + } + } label: { + Label("status.menu.share-image", systemImage: "photo") + } + + Divider() + + Button { + UIPasteboard.general.setValue(status.reblogAsAsStatus?.content.asRawText ?? status.content.asRawText, forPasteboardType: UTType.plainText.identifier) + } label: { + Label("status.menu.copy-text", systemImage: "list.clipboard") } } label: { - Image(systemName: "ellipsis") - .foregroundStyle(Color.white.opacity(0.3)) - .font(.body) - .contentShape(Rectangle()) - .padding(7.5) + Label("status.menu.share", systemImage: "paperplane") + } + + Divider() + + Button(role: .destructive) { + navigator.presentedSheet = .reportStatus(status: status) + } label: { + Label("status.menu.report", systemImage: "exclamationmark.triangle.fill") } } diff --git a/Threaded/Data/Navigator.swift b/Threaded/Data/Navigator.swift index e12e639..5d2fea4 100644 --- a/Threaded/Data/Navigator.swift +++ b/Threaded/Data/Navigator.swift @@ -107,7 +107,10 @@ public enum SheetDestination: Identifiable { case shareImage(image: UIImage, status: Status) case update case filter - + + case reportStatus(status: Status) +// case reportUser + public var id: String { switch self { case .welcome: @@ -131,6 +134,9 @@ public enum SheetDestination: Identifiable { return "update" case .filter: return "contentfilter" + + case .reportStatus: + return "reportStatus" } } @@ -157,6 +163,9 @@ public enum SheetDestination: Identifiable { return false case .filter: return false + + case .reportStatus: + return false } } } @@ -260,6 +269,8 @@ extension View { ShareSheet(image: image, status: status) case .update: UpdateView() + case let .reportStatus(status): + ReportStatusView(status: status) default: EmptySheetView(destId: destination.id) } diff --git a/Threaded/Localizable.xcstrings b/Threaded/Localizable.xcstrings index df51bfb..f95c231 100644 --- a/Threaded/Localizable.xcstrings +++ b/Threaded/Localizable.xcstrings @@ -1461,6 +1461,70 @@ } } }, + "general.report.confirm" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Send the report?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Envoyer ce signalement ?" + } + } + } + }, + "general.report.confirm.cancel" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuler" + } + } + } + }, + "general.report.confirm.message" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you falsely report a post or an account, your own account could face consequences. Remember to report only content that is against the rules." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Si vous faites un faux signalement, votre compte pourrait subir des conséquences. Rappelez-vous de signaler seulement le contenu qui est contre les règles." + } + } + } + }, + "general.report.confirm.ok" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + } + } + }, "instance.rules" : { "localizations" : { "en" : { @@ -3168,6 +3232,22 @@ } } }, + "status.menu.report" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Report" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signaler" + } + } + } + }, "status.menu.share" : { "localizations" : { "en" : { @@ -3644,6 +3724,102 @@ } } }, + "status.report" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Report" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signaler" + } + } + } + }, + "status.report.cancel" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annuler" + } + } + } + }, + "status.report.comment" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Describe the issue with this post" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Commentez les soucis de cette publication" + } + } + } + }, + "status.report.preview" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selected Post" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Post sélectionné" + } + } + } + }, + "status.report.preview.footer" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This preview does not contain polls, URL previews, the author’s profile picture." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cette prévisualisation ne contient pas les sondages, prévisualisations d’URLs, la photo de profil de l’auteur" + } + } + } + }, + "status.report.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Report a post" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signaler une publication" + } + } + } + }, "status.reposted-by.%@" : { "localizations" : { "en" : { @@ -4218,4 +4394,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Threaded/Views/PostDetailsView.swift b/Threaded/Views/PostDetailsView.swift index 8a374d3..65eed4b 100644 --- a/Threaded/Views/PostDetailsView.swift +++ b/Threaded/Views/PostDetailsView.swift @@ -73,8 +73,16 @@ struct PostDetailsView: View { } Spacer() - - PostMenu(status: status) + + Menu { + PostMenu(status: status) + } label: { + Image(systemName: "ellipsis") + .foregroundStyle(Color.white.opacity(0.3)) + .font(.body) + .contentShape(Rectangle()) + .padding(7.5) + } .padding([.trailing, .top]) } diff --git a/Threaded/Views/ReportStatusView.swift b/Threaded/Views/ReportStatusView.swift new file mode 100644 index 0000000..8f57255 --- /dev/null +++ b/Threaded/Views/ReportStatusView.swift @@ -0,0 +1,172 @@ +// Made by Lumaa + +import SwiftUI + +struct ReportStatusView: View { + @Environment(AccountManager.self) private var accountManager: AccountManager + @Environment(\.dismiss) private var dismiss: DismissAction + + var status: Status + + @State private var comment: String = "" + @State private var confirmationAlert: Bool = false + + var body: some View { + NavigationStack { + Form { + Section { + TextField("status.report.comment", text: $comment, axis: .vertical) + .frame(maxHeight: 300) + + Button { + confirmationAlert.toggle() + } label: { + Text("status.report") + .foregroundStyle(Color.red) + .bold() + } + } + + Section(header: Text("status.report.preview"), footer: Text("status.report.preview.footer")) { + StatusPreview(status: status) + } + } + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + dismiss() + } label: { + Text("status.report.cancel") + } + } + +// ToolbarItem(placement: .confirmationAction) { +// Button { +// confirmationAlert.toggle() +// } label: { +// Text("status.report") +// .foregroundStyle(Color.red) +// } +// } + } + .alert( + "general.report.confirm", + isPresented: $confirmationAlert, + actions: { + Button(role: .destructive) { + Task { + if comment.isEmpty { + await reportStatus() + } else { + await reportStatus(comment: comment) + } + HapticManager.playHaptics(haptics: Haptic.success) + dismiss() + } + } label: { + Text("general.report.confirm.ok") + } + + Button(role: .cancel) {} label: { + Text("general.report.confirm.cancel") + } + }, + message: { Text("general.report.confirm.message") } + ) + .navigationTitle(Text("status.report.title")) + .navigationBarTitleDisplayMode(.large) + } + } + + private func reportStatus(comment: String = "*No information was given*") async { + if let client = accountManager.getClient() { + _ = try? await client + .post( + endpoint: Statuses + .report( + accountId: status.account.id, + statusId: status.id, + comment: comment + ) + ) + } + } + + struct StatusPreview: View { + var status: Status + + var body: some View { + HStack(alignment: .top, spacing: 0) { + VStack(alignment: .leading) { + // MARK: Status main content + VStack(alignment: .leading, spacing: 10) { + VStack(alignment: .leading, spacing: 2) { + Text("@\(status.account.acct)") + .font(.callout) + .multilineTextAlignment(.leading) + .bold() + + if status.inReplyToAccountId != nil { + if let user = status.mentions.first(where: { $0.id == status.inReplyToAccountId }) { + Text("status.replied-to.\(user.username)") + .multilineTextAlignment(.leading) + .lineLimit(1) + .font(.caption) + .foregroundStyle(Color(uiColor: UIColor.label).opacity(0.3)) + } + } + } + + if !status.content.asRawText.isEmpty { + TextEmoji(status.content, emojis: status.emojis, language: status.language) + .multilineTextAlignment(.leading) + .frame(width: 300, alignment: .topLeading) + .fixedSize(horizontal: false, vertical: true) + .font(.callout) + .contentShape(Rectangle()) + } + + attachmnts + } + } + } + } + + @ViewBuilder + var attachmnts: some View { + if !status.mediaAttachments.isEmpty { + if status.mediaAttachments.count > 1 { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .firstTextBaseline, spacing: 5) { + ForEach(status.mediaAttachments) { attachment in + PostAttachment(attachment: attachment, isFeatured: false) + .blur(radius: status.sensitive ? 15.0 : 0) + } + } + } + .scrollClipDisabled() + } else { + PostAttachment(attachment: status.mediaAttachments.first!) + } + } + } + } +} + +#Preview("FR") { + ReportStatusView(status: .placeholder()) + .environment(AccountManager()) + .environment(\.locale, Locale(identifier: "fr-fr")) +} + +#Preview("Sheet") { + ZStack { + Text(String("Hello world!")) + } + .interactiveDismissDisabled() + .presentationDragIndicator(.hidden) + .sheet(isPresented: .constant(true)) { + ReportStatusView(status: .placeholder()) + .environment(AccountManager()) + } +}