Better quote post

This commit is contained in:
Thomas Ricouard 2022-12-27 13:38:10 +01:00
parent 816e1d5e7d
commit 0ac109c49b
8 changed files with 141 additions and 97 deletions

View file

@ -397,7 +397,7 @@
CODE_SIGN_IDENTITY = "-"; CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 500; CURRENT_PROJECT_VERSION = 550;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\""; DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\"";
DEVELOPMENT_TEAM = Z6P74P6T99; DEVELOPMENT_TEAM = Z6P74P6T99;
@ -443,7 +443,7 @@
CODE_SIGN_IDENTITY = "-"; CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 500; CURRENT_PROJECT_VERSION = 550;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\""; DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\"";
DEVELOPMENT_TEAM = Z6P74P6T99; DEVELOPMENT_TEAM = Z6P74P6T99;

View file

@ -51,7 +51,7 @@
"location" : "https://github.com/Dimillian/TextView", "location" : "https://github.com/Dimillian/TextView",
"state" : { "state" : {
"branch" : "main", "branch" : "main",
"revision" : "3555eecb81f918091d4f65c071dd94e64995b41b" "revision" : "26b2930e82bb379a4abf0fcba408c0a09fbbb407"
} }
} }
], ],

View file

@ -16,9 +16,9 @@ public enum RouteurDestinations: Hashable {
public enum SheetDestinations: Identifiable { public enum SheetDestinations: Identifiable {
case newStatusEditor case newStatusEditor
case editStatusEditor(status: AnyStatus) case editStatusEditor(status: Status)
case replyToStatusEditor(status: AnyStatus) case replyToStatusEditor(status: Status)
case quoteStatusEditor(status: AnyStatus) case quoteStatusEditor(status: Status)
public var id: String { public var id: String {
switch self { switch self {

View file

@ -21,13 +21,18 @@ public struct StatusEditorView: View {
public var body: some View { public var body: some View {
NavigationStack { NavigationStack {
ZStack(alignment: .bottom) { ZStack(alignment: .bottom) {
ScrollView {
VStack(spacing: 12) { VStack(spacing: 12) {
accountHeaderView accountHeaderView
TextView($viewModel.statusText) TextView($viewModel.statusText)
.placeholder("What's on your mind") .placeholder("What's on your mind")
if let status = viewModel.embededStatus {
StatusEmbededView(status: status)
}
mediasView mediasView
Spacer() Spacer()
} }
}
accessoryView accessoryView
.padding(.bottom, 12) .padding(.bottom, 12)
} }

View file

@ -6,41 +6,20 @@ import PhotosUI
@MainActor @MainActor
public class StatusEditorViewModel: ObservableObject { public class StatusEditorViewModel: ObservableObject {
public enum Mode { struct ImageContainer: Identifiable {
case replyTo(status: AnyStatus) let id = UUID().uuidString
case new let image: UIImage
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 { var mode: Mode
switch self { let generator = UINotificationFeedbackGenerator()
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)"
}
}
}
let mode: Mode var client: Client?
@Published var statusText = NSAttributedString(string: "") { @Published var statusText = NSMutableAttributedString(string: "") {
didSet { didSet {
guard !internalUpdate else { return }
highlightMeta() highlightMeta()
checkEmbed()
} }
} }
@ -52,15 +31,7 @@ public class StatusEditorViewModel: ObservableObject {
} }
@Published var mediasImages: [ImageContainer] = [] @Published var mediasImages: [ImageContainer] = []
struct ImageContainer: Identifiable { @Published var embededStatus: Status?
let id = UUID().uuidString
let image: UIImage
}
var client: Client?
private var internalUpdate: Bool = false
let generator = UINotificationFeedbackGenerator()
init(mode: Mode) { init(mode: Mode) {
self.mode = mode self.mode = mode
@ -96,62 +67,65 @@ public class StatusEditorViewModel: ObservableObject {
func prepareStatusText() { func prepareStatusText() {
switch mode { switch mode {
case let .replyTo(status): case let .replyTo(status):
statusText = .init(string: "@\(status.account.acct) ") statusText = .init(string: "@\(status.reblog?.account.acct ?? status.account.acct) ")
case let .edit(status): case let .edit(status):
statusText = .init(string: status.content.asRawText) statusText = .init(status.content.asSafeAttributedString)
case let .quote(status): case let .quote(status):
if let url = status.url { self.embededStatus = status
statusText = .init(string: "\n\nFrom: @\(status.account.acct)\n\(url)") if let url = status.reblog?.url ?? status.url {
statusText = .init(string: "\n\nFrom: @\(status.reblog?.account.acct ?? status.account.acct)\n\(url)")
} }
default: default:
break break
} }
} }
func highlightMeta() { private func highlightMeta() {
let mutableString = NSMutableAttributedString(string: statusText.string) statusText.addAttributes([.foregroundColor: UIColor(Color.label)],
mutableString.addAttributes([.foregroundColor: UIColor(Color.label)], range: NSMakeRange(0, statusText.string.utf16.count))
range: NSMakeRange(0, mutableString.string.utf16.count))
let hashtagPattern = "(#+[a-zA-Z0-9(_)]{1,})" let hashtagPattern = "(#+[a-zA-Z0-9(_)]{1,})"
let mentionPattern = "(@+[a-zA-Z0-9(_).]{1,})" let mentionPattern = "(@+[a-zA-Z0-9(_).]{1,})"
let urlPattern = "(?i)https?://(?:www\\.)?\\S+(?:/|\\b)" let urlPattern = "(?i)https?://(?:www\\.)?\\S+(?:/|\\b)"
var ranges: [NSRange] = [NSRange]()
do { do {
let hashtagRegex = try NSRegularExpression(pattern: hashtagPattern, options: []) let hashtagRegex = try NSRegularExpression(pattern: hashtagPattern, options: [])
let mentionRegex = try NSRegularExpression(pattern: mentionPattern, options: []) let mentionRegex = try NSRegularExpression(pattern: mentionPattern, options: [])
let urlRegex = try NSRegularExpression(pattern: urlPattern, options: []) let urlRegex = try NSRegularExpression(pattern: urlPattern, options: [])
ranges = hashtagRegex.matches(in: mutableString.string, var ranges = hashtagRegex.matches(in: statusText.string,
options: [], options: [],
range: NSMakeRange(0, mutableString.string.utf16.count)).map { $0.range } range: NSMakeRange(0, statusText.string.utf16.count)).map { $0.range }
ranges.append(contentsOf: mentionRegex.matches(in: mutableString.string, ranges.append(contentsOf: mentionRegex.matches(in: statusText.string,
options: [], 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: [], 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 { for range in ranges {
mutableString.addAttributes([.foregroundColor: UIColor(Color.brand)], statusText.addAttributes([.foregroundColor: UIColor(Color.brand)],
range: NSRange(location: range.location, length: range.length)) range: NSRange(location: range.location, length: range.length))
} }
for range in urlRanges { for range in urlRanges {
mutableString.addAttributes([.foregroundColor: UIColor(Color.brand), statusText.addAttributes([.foregroundColor: UIColor(Color.brand),
.underlineStyle: NSUnderlineStyle.single, .underlineStyle: NSUnderlineStyle.single,
.underlineColor: UIColor(Color.brand)], .underlineColor: UIColor(Color.brand)],
range: NSRange(location: range.location, length: range.length)) range: NSRange(location: range.location, length: range.length))
} }
internalUpdate = true
statusText = mutableString
internalUpdate = false
} catch { } catch {
} }
} }
private func checkEmbed() {
if let embededStatus, !statusText.string.contains(embededStatus.reblog?.id ?? embededStatus.id) {
self.embededStatus = nil
self.mode = .new
}
}
func inflateSelectedMedias() { func inflateSelectedMedias() {
for media in selectedMedias { for media in selectedMedias {
media.loadTransferable(type: Data.self) { [weak self] result in media.loadTransferable(type: Data.self) { [weak self] result in

View file

@ -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)"
}
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -89,7 +89,7 @@ public struct StatusRowView: View {
Button { Button {
routeurPath.navigate(to: .accountDetailWithAccount(account: status.account)) routeurPath.navigate(to: .accountDetailWithAccount(account: status.account))
} label: { } label: {
makeAccountView(status: status) accountView(status: status)
}.buttonStyle(.plain) }.buttonStyle(.plain)
Spacer() Spacer()
menuButton menuButton
@ -108,7 +108,9 @@ public struct StatusRowView: View {
routeurPath.handleStatus(status: status, url: url) routeurPath.handleStatus(status: status, url: url)
}) })
embededStatusView if !viewModel.isEmbed, let embed = viewModel.embededStatus {
StatusEmbededView(status: embed)
}
if !status.mediaAttachments.isEmpty { if !status.mediaAttachments.isEmpty {
if viewModel.isEmbed { if viewModel.isEmbed {
@ -129,41 +131,24 @@ public struct StatusRowView: View {
} }
@ViewBuilder @ViewBuilder
private func makeAccountView(status: AnyStatus, size: AvatarView.Size = .status) -> some View { private func accountView(status: AnyStatus) -> some View {
HStack(alignment: .center) { HStack(alignment: .center) {
AvatarView(url: status.account.avatar, size: size) AvatarView(url: status.account.avatar, size: .status)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
status.account.displayNameWithEmojis status.account.displayNameWithEmojis
.font(size == .embed ? .footnote : .headline) .font(.headline)
.fontWeight(.semibold) .fontWeight(.semibold)
Group { Group {
Text("@\(status.account.acct)") + Text("@\(status.account.acct)") +
Text("") + Text("") +
Text(status.createdAt.formatted) Text(status.createdAt.formatted)
} }
.font(size == .embed ? .caption : .footnote) .font(.footnote)
.foregroundColor(.gray) .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 { private var menuButton: some View {
Menu { Menu {
contextMenu contextMenu
@ -196,9 +181,9 @@ public struct StatusRowView: View {
Label(viewModel.isReblogged ? "Unboost" : "Boost", systemImage: "arrow.left.arrow.right.circle") Label(viewModel.isReblogged ? "Unboost" : "Boost", systemImage: "arrow.left.arrow.right.circle")
} }
Button { Button {
routeurPath.presentedSheet = .quoteStatusEditor(status: viewModel.status.reblog ?? viewModel.status) routeurPath.presentedSheet = .quoteStatusEditor(status: viewModel.status)
} label: { } 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 { if let url = viewModel.status.reblog?.url ?? viewModel.status.url {