mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-05 16:59:30 +00:00
Better quote post
This commit is contained in:
parent
816e1d5e7d
commit
0ac109c49b
8 changed files with 141 additions and 97 deletions
|
@ -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;
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"location" : "https://github.com/Dimillian/TextView",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "3555eecb81f918091d4f65c071dd94e64995b41b"
|
||||
"revision" : "26b2930e82bb379a4abf0fcba408c0a09fbbb407"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
Packages/Status/Sources/Status/Embed/StatusEmbededView.swift
Normal file
48
Packages/Status/Sources/Status/Embed/StatusEmbededView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue