diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 376b7dda..59bd11ae 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -397,7 +397,7 @@ CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 500; + CURRENT_PROJECT_VERSION = 550; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\""; DEVELOPMENT_TEAM = Z6P74P6T99; @@ -443,7 +443,7 @@ CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 500; + CURRENT_PROJECT_VERSION = 550; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\""; DEVELOPMENT_TEAM = Z6P74P6T99; diff --git a/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ddc76ffe..3f75e205 100644 --- a/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -51,7 +51,7 @@ "location" : "https://github.com/Dimillian/TextView", "state" : { "branch" : "main", - "revision" : "3555eecb81f918091d4f65c071dd94e64995b41b" + "revision" : "26b2930e82bb379a4abf0fcba408c0a09fbbb407" } } ], diff --git a/Packages/Env/Sources/Env/Routeur.swift b/Packages/Env/Sources/Env/Routeur.swift index 428b8089..87a8ce38 100644 --- a/Packages/Env/Sources/Env/Routeur.swift +++ b/Packages/Env/Sources/Env/Routeur.swift @@ -16,9 +16,9 @@ public enum RouteurDestinations: Hashable { public enum SheetDestinations: Identifiable { case newStatusEditor - case editStatusEditor(status: AnyStatus) - case replyToStatusEditor(status: AnyStatus) - case quoteStatusEditor(status: AnyStatus) + case editStatusEditor(status: Status) + case replyToStatusEditor(status: Status) + case quoteStatusEditor(status: Status) public var id: String { switch self { diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index d038986c..5cff982b 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -21,12 +21,17 @@ public struct StatusEditorView: View { public var body: some View { NavigationStack { ZStack(alignment: .bottom) { - VStack(spacing: 12) { - accountHeaderView - TextView($viewModel.statusText) - .placeholder("What's on your mind") - mediasView - Spacer() + ScrollView { + VStack(spacing: 12) { + accountHeaderView + TextView($viewModel.statusText) + .placeholder("What's on your mind") + if let status = viewModel.embededStatus { + StatusEmbededView(status: status) + } + mediasView + Spacer() + } } accessoryView .padding(.bottom, 12) diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index e27c0716..87e53c1c 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -6,41 +6,20 @@ import PhotosUI @MainActor public class StatusEditorViewModel: ObservableObject { - public enum Mode { - case replyTo(status: AnyStatus) - case new - case edit(status: AnyStatus) - case quote(status: AnyStatus) - - var replyToStatus: AnyStatus? { - switch self { - case let .replyTo(status): - return status - default: - return nil - } - } - - var title: String { - switch self { - case .new: - return "New Post" - case .edit: - return "Edit your post" - case let .replyTo(status): - return "Reply to \(status.account.displayName)" - case let .quote(status): - return "Quote of \(status.account.displayName)" - } - } + struct ImageContainer: Identifiable { + let id = UUID().uuidString + let image: UIImage } - let mode: Mode + var mode: Mode + let generator = UINotificationFeedbackGenerator() - @Published var statusText = NSAttributedString(string: "") { + var client: Client? + + @Published var statusText = NSMutableAttributedString(string: "") { didSet { - guard !internalUpdate else { return } highlightMeta() + checkEmbed() } } @@ -52,16 +31,8 @@ public class StatusEditorViewModel: ObservableObject { } @Published var mediasImages: [ImageContainer] = [] - struct ImageContainer: Identifiable { - let id = UUID().uuidString - let image: UIImage - } - - var client: Client? - private var internalUpdate: Bool = false - - let generator = UINotificationFeedbackGenerator() - + @Published var embededStatus: Status? + init(mode: Mode) { self.mode = mode } @@ -96,62 +67,65 @@ public class StatusEditorViewModel: ObservableObject { func prepareStatusText() { switch mode { case let .replyTo(status): - statusText = .init(string: "@\(status.account.acct) ") + statusText = .init(string: "@\(status.reblog?.account.acct ?? status.account.acct) ") case let .edit(status): - statusText = .init(string: status.content.asRawText) + statusText = .init(status.content.asSafeAttributedString) case let .quote(status): - if let url = status.url { - statusText = .init(string: "\n\nFrom: @\(status.account.acct)\n\(url)") + self.embededStatus = status + if let url = status.reblog?.url ?? status.url { + statusText = .init(string: "\n\nFrom: @\(status.reblog?.account.acct ?? status.account.acct)\n\(url)") } default: break } } - func highlightMeta() { - let mutableString = NSMutableAttributedString(string: statusText.string) - mutableString.addAttributes([.foregroundColor: UIColor(Color.label)], - range: NSMakeRange(0, mutableString.string.utf16.count)) + private func highlightMeta() { + statusText.addAttributes([.foregroundColor: UIColor(Color.label)], + range: NSMakeRange(0, statusText.string.utf16.count)) let hashtagPattern = "(#+[a-zA-Z0-9(_)]{1,})" let mentionPattern = "(@+[a-zA-Z0-9(_).]{1,})" let urlPattern = "(?i)https?://(?:www\\.)?\\S+(?:/|\\b)" - var ranges: [NSRange] = [NSRange]() do { let hashtagRegex = try NSRegularExpression(pattern: hashtagPattern, options: []) let mentionRegex = try NSRegularExpression(pattern: mentionPattern, options: []) let urlRegex = try NSRegularExpression(pattern: urlPattern, options: []) - ranges = hashtagRegex.matches(in: mutableString.string, + var ranges = hashtagRegex.matches(in: statusText.string, options: [], - range: NSMakeRange(0, mutableString.string.utf16.count)).map { $0.range } - ranges.append(contentsOf: mentionRegex.matches(in: mutableString.string, + range: NSMakeRange(0, statusText.string.utf16.count)).map { $0.range } + ranges.append(contentsOf: mentionRegex.matches(in: statusText.string, options: [], - range: NSMakeRange(0, mutableString.string.utf16.count)).map {$0.range}) + range: NSMakeRange(0, statusText.string.utf16.count)).map {$0.range}) - let urlRanges = urlRegex.matches(in: mutableString.string, + let urlRanges = urlRegex.matches(in: statusText.string, options: [], - range: NSMakeRange(0, mutableString.string.utf16.count)).map { $0.range } + range: NSMakeRange(0, statusText.string.utf16.count)).map { $0.range } for range in ranges { - mutableString.addAttributes([.foregroundColor: UIColor(Color.brand)], + statusText.addAttributes([.foregroundColor: UIColor(Color.brand)], range: NSRange(location: range.location, length: range.length)) } for range in urlRanges { - mutableString.addAttributes([.foregroundColor: UIColor(Color.brand), + statusText.addAttributes([.foregroundColor: UIColor(Color.brand), .underlineStyle: NSUnderlineStyle.single, .underlineColor: UIColor(Color.brand)], range: NSRange(location: range.location, length: range.length)) } - internalUpdate = true - statusText = mutableString - internalUpdate = false } catch { } } + private func checkEmbed() { + if let embededStatus, !statusText.string.contains(embededStatus.reblog?.id ?? embededStatus.id) { + self.embededStatus = nil + self.mode = .new + } + } + func inflateSelectedMedias() { for media in selectedMedias { media.loadTransferable(type: Data.self) { [weak self] result in diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModelMode.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModelMode.swift new file mode 100644 index 00000000..faf2860e --- /dev/null +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModelMode.swift @@ -0,0 +1,32 @@ +import Models + +extension StatusEditorViewModel { + public enum Mode { + case replyTo(status: Status) + case new + case edit(status: Status) + case quote(status: Status) + + var replyToStatus: Status? { + switch self { + case let .replyTo(status): + return status + default: + return nil + } + } + + var title: String { + switch self { + case .new: + return "New Post" + case .edit: + return "Edit your post" + case let .replyTo(status): + return "Reply to \(status.reblog?.account.displayName ?? status.account.displayName)" + case let .quote(status): + return "Quote of \(status.reblog?.account.displayName ?? status.account.displayName)" + } + } + } +} diff --git a/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift b/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift new file mode 100644 index 00000000..289a0a7c --- /dev/null +++ b/Packages/Status/Sources/Status/Embed/StatusEmbededView.swift @@ -0,0 +1,48 @@ +import SwiftUI +import Models +import DesignSystem + +@MainActor +public struct StatusEmbededView: View { + public let status: Status + + public init(status: Status) { + self.status = status + } + + public var body: some View { + HStack { + VStack(alignment: .leading) { + makeAccountView(account: status.reblog?.account ?? status.account) + StatusRowView(viewModel: .init(status: status, isEmbed: true)) + } + Spacer() + } + .padding(8) + .background(Color.gray.opacity(0.10)) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(.gray.opacity(0.35), lineWidth: 1) + ) + .padding(.top, 8) + } + + private func makeAccountView(account: Account) -> some View { + HStack(alignment: .center) { + AvatarView(url: account.avatar, size: .embed) + VStack(alignment: .leading, spacing: 0) { + status.account.displayNameWithEmojis + .font(.footnote) + .fontWeight(.semibold) + Group { + Text("@\(account.acct)") + + Text(" ⸱ ") + + Text(status.reblog?.createdAt.formatted ?? status.createdAt.formatted) + } + .font(.caption) + .foregroundColor(.gray) + } + } + } +} diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index 6f89cc01..37f653ff 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -89,7 +89,7 @@ public struct StatusRowView: View { Button { routeurPath.navigate(to: .accountDetailWithAccount(account: status.account)) } label: { - makeAccountView(status: status) + accountView(status: status) }.buttonStyle(.plain) Spacer() menuButton @@ -108,7 +108,9 @@ public struct StatusRowView: View { routeurPath.handleStatus(status: status, url: url) }) - embededStatusView + if !viewModel.isEmbed, let embed = viewModel.embededStatus { + StatusEmbededView(status: embed) + } if !status.mediaAttachments.isEmpty { if viewModel.isEmbed { @@ -129,41 +131,24 @@ public struct StatusRowView: View { } @ViewBuilder - private func makeAccountView(status: AnyStatus, size: AvatarView.Size = .status) -> some View { + private func accountView(status: AnyStatus) -> some View { HStack(alignment: .center) { - AvatarView(url: status.account.avatar, size: size) + AvatarView(url: status.account.avatar, size: .status) VStack(alignment: .leading, spacing: 0) { status.account.displayNameWithEmojis - .font(size == .embed ? .footnote : .headline) + .font(.headline) .fontWeight(.semibold) Group { Text("@\(status.account.acct)") + Text(" ⸱ ") + Text(status.createdAt.formatted) } - .font(size == .embed ? .caption : .footnote) + .font(.footnote) .foregroundColor(.gray) } } } - @ViewBuilder - private var embededStatusView: some View { - if let status = viewModel.embededStatus { - VStack(alignment: .leading) { - makeAccountView(status: status, size: .embed) - StatusRowView(viewModel: .init(status: status, isEmbed: true)) - } - .padding(8) - .background(Color.gray.opacity(0.10)) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(.gray.opacity(0.35), lineWidth: 1) - ) - .padding(.top, 8) - } - } - private var menuButton: some View { Menu { contextMenu @@ -196,9 +181,9 @@ public struct StatusRowView: View { Label(viewModel.isReblogged ? "Unboost" : "Boost", systemImage: "arrow.left.arrow.right.circle") } Button { - routeurPath.presentedSheet = .quoteStatusEditor(status: viewModel.status.reblog ?? viewModel.status) + routeurPath.presentedSheet = .quoteStatusEditor(status: viewModel.status) } label: { - Label("Quote this status", systemImage: "quote.bubble") + Label("Quote this post", systemImage: "quote.bubble") } if let url = viewModel.status.reblog?.url ?? viewModel.status.url {