diff --git a/Packages/Models/Sources/Models/Status.swift b/Packages/Models/Sources/Models/Status.swift index 7ff32711..a2042faa 100644 --- a/Packages/Models/Sources/Models/Status.swift +++ b/Packages/Models/Sources/Models/Status.swift @@ -37,6 +37,7 @@ public protocol AnyStatus { var inReplyToAccountId: String? { get } var visibility: Visibility { get } var poll: Poll? { get } + var spoilerText: String { get } } @@ -66,6 +67,7 @@ public struct Status: AnyStatus, Codable, Identifiable { public let inReplyToAccountId: String? public let visibility: Visibility public let poll: Poll? + public let spoilerText: String public static func placeholder() -> Status { .init(id: UUID().uuidString, @@ -88,7 +90,8 @@ public struct Status: AnyStatus, Codable, Identifiable { application: nil, inReplyToAccountId: nil, visibility: .pub, - poll: nil) + poll: nil, + spoilerText: "") } public static func placeholders() -> [Status] { @@ -121,4 +124,5 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable { public let inReplyToAccountId: String? public let visibility: Visibility public let poll: Poll? + public let spoilerText: String } diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index 446823de..0b4769d1 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -16,6 +16,7 @@ public struct StatusEditorView: View { @Environment(\.dismiss) private var dismiss @StateObject private var viewModel: StatusEditorViewModel + @FocusState private var isSpoilerTextFocused: Bool public init(mode: StatusEditorViewModel.Mode) { _viewModel = StateObject(wrappedValue: .init(mode: mode)) @@ -26,6 +27,7 @@ public struct StatusEditorView: View { ZStack(alignment: .bottom) { ScrollView { Divider() + spoilerTextView VStack(spacing: 12) { accountHeaderView .padding(.horizontal, DS.Constants.layoutPadding) @@ -80,6 +82,20 @@ public struct StatusEditorView: View { } } + @ViewBuilder + private var spoilerTextView: some View { + if viewModel.spoilerOn { + VStack { + TextField("Spoiler Text", text: $viewModel.spoilerText) + .focused($isSpoilerTextFocused) + .padding(.horizontal, DS.Constants.layoutPadding) + } + .frame(height: 35) + .background(Color.brand.opacity(0.20)) + .offset(y: -8) + } + } + @ViewBuilder private var accountHeaderView: some View { if let account = currentAccount.account { @@ -179,7 +195,7 @@ public struct StatusEditorView: View { private var accessoryView: some View { VStack(spacing: 0) { Divider() - HStack(spacing: 16) { + HStack(alignment: .center, spacing: 16) { PhotosPicker(selection: $viewModel.selectedMedias, matching: .images) { Image(systemName: "photo.fill.on.rectangle.fill") @@ -197,12 +213,22 @@ public struct StatusEditorView: View { Image(systemName: "number") } + Button { + withAnimation { + viewModel.spoilerOn.toggle() + } + isSpoilerTextFocused.toggle() + } label: { + Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill": "exclamationmark.triangle") + } + visibilityMenu Spacer() characterCountView } + .frame(height: 20) .padding(.horizontal, DS.Constants.layoutPadding) .padding(.vertical, 12) .background(.ultraThinMaterial) @@ -227,7 +253,6 @@ public struct StatusEditorView: View { } label: { Image(systemName: viewModel.visibility.iconName) } - } } diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index 3c203d2e..157a33cb 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -25,6 +25,9 @@ public class StatusEditorViewModel: ObservableObject { } } + @Published var spoilerOn: Bool = false + @Published var spoilerText: String = "" + @Published var selectedRange: NSRange = .init(location: 0, length: 0) @Published var isPosting: Bool = false @@ -64,13 +67,13 @@ public class StatusEditorViewModel: ObservableObject { postStatus = try await client.post(endpoint: Statuses.postStatus(status: statusText.string, inReplyTo: mode.replyToStatus?.id, mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id }, - spoilerText: nil, + spoilerText: spoilerOn ? spoilerText : nil, visibility: visibility)) case let .edit(status): postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id, status: statusText.string, mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id }, - spoilerText: nil, + spoilerText: spoilerOn ? spoilerText : nil, visibility: visibility)) } generator.notificationOccurred(.success) @@ -90,7 +93,10 @@ public class StatusEditorViewModel: ObservableObject { selectedRange = .init(location: statusText.string.utf16.count, length: 0) case let .edit(status): statusText = .init(status.content.asSafeAttributedString) - selectedRange = .init(location: 0, length: 0) + selectedRange = .init(location: statusText.string.utf16.count, length: 0) + spoilerOn = !status.spoilerText.isEmpty + spoilerText = status.spoilerText + mediasImages = status.mediaAttachments.map{ .init(image: nil, mediaAttachement: $0, error: nil )} case let .quote(status): self.embededStatus = status if let url = status.reblog?.url ?? status.url { diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index e70c57a7..30a59083 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -67,17 +67,17 @@ public struct StatusRowView: View { var replyView: some View { if let accountId = viewModel.status.inReplyToAccountId, let mention = viewModel.status.mentions.first(where: { $0.id == accountId}) { - HStack(spacing: 2) { - Image(systemName:"arrowshape.turn.up.left.fill") - Text("Replied to") - Text(mention.username) - } - .font(.footnote) - .foregroundColor(.gray) - .fontWeight(.semibold) - .onTapGesture { - routeurPath.navigate(to: .accountDetail(id: mention.id)) - } + HStack(spacing: 2) { + Image(systemName:"arrowshape.turn.up.left.fill") + Text("Replied to") + Text(mention.username) + } + .font(.footnote) + .foregroundColor(.gray) + .fontWeight(.semibold) + .onTapGesture { + routeurPath.navigate(to: .accountDetail(id: mention.id)) + } } } @@ -96,17 +96,26 @@ public struct StatusRowView: View { } } makeStatusContentView(status: status) + .padding(.vertical, viewModel.displaySpoiler ? 28 : 0) + .overlay { + if viewModel.displaySpoiler { + spoilerView + } + } } } } private func makeStatusContentView(status: AnyStatus) -> some View { Group { - Text(status.content.asSafeAttributedString) - .font(.body) - .environment(\.openURL, OpenURLAction { url in - routeurPath.handleStatus(status: status, url: url) - }) + HStack { + Text(status.content.asSafeAttributedString) + .font(.body) + .environment(\.openURL, OpenURLAction { url in + routeurPath.handleStatus(status: status, url: url) + }) + Spacer() + } if !viewModel.isEmbed, let embed = viewModel.embededStatus { StatusEmbededView(status: embed) @@ -191,13 +200,13 @@ public struct StatusRowView: View { } label: { Label("Quote this post", systemImage: "quote.bubble") } - + if let url = viewModel.status.reblog?.url ?? viewModel.status.url { Button { UIApplication.shared.open(url) } label: { Label("View in Browser", systemImage: "safari") } } - + if account.account?.id == viewModel.status.account.id { Button { routeurPath.presentedSheet = .editStatusEditor(status: viewModel.status) @@ -209,4 +218,29 @@ public struct StatusRowView: View { } } } + + private var spoilerView: some View { + HStack { + Spacer() + VStack { + Spacer() + Text(viewModel.status.reblog?.spoilerText ?? viewModel.status.spoilerText) + .font(.callout) + Button { + withAnimation { + viewModel.displaySpoiler = false + } + } label: { + Text("See more") + } + .buttonStyle(.bordered) + + Spacer() + } + Spacer() + } + .background(.ultraThinMaterial) + .transition(.opacity) + .cornerRadius(4) + } } diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index 5d0aead3..4973012d 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -14,6 +14,7 @@ public class StatusRowViewModel: ObservableObject { @Published var reblogsCount: Int @Published var repliesCount: Int @Published var embededStatus: Status? + @Published var displaySpoiler: Bool = false var client: Client? @@ -33,6 +34,7 @@ public class StatusRowViewModel: ObservableObject { self.favouritesCount = status.reblog?.favouritesCount ?? status.favouritesCount self.reblogsCount = status.reblog?.reblogsCount ?? status.reblogsCount self.repliesCount = status.reblog?.repliesCount ?? status.repliesCount + self.displaySpoiler = !status.spoilerText.isEmpty } func loadEmbededStatus() async {