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[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;

View file

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

View file

@ -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 {

View file

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

View file

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

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 {
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 {