mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-06 01:09: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 = "-";
|
||||||
"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;
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
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 {
|
||||||
|
|
Loading…
Reference in a new issue