IceCubesApp/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift

337 lines
12 KiB
Swift
Raw Normal View History

2022-12-25 05:55:33 +00:00
import SwiftUI
import DesignSystem
2022-12-25 07:17:16 +00:00
import Models
import Network
2022-12-25 18:15:35 +00:00
import PhotosUI
2022-12-25 05:55:33 +00:00
@MainActor
2022-12-26 07:24:55 +00:00
public class StatusEditorViewModel: ObservableObject {
2022-12-27 12:38:10 +00:00
struct ImageContainer: Identifiable {
let id = UUID().uuidString
2022-12-27 15:16:25 +00:00
let image: UIImage?
let mediaAttachement: MediaAttachement?
let error: Error?
2022-12-26 07:24:55 +00:00
}
2022-12-27 12:38:10 +00:00
var mode: Mode
let generator = UINotificationFeedbackGenerator()
var client: Client?
2022-12-30 21:49:09 +00:00
var currentAccount: Account?
2022-12-31 11:11:42 +00:00
var theme: Theme?
2022-12-26 07:24:55 +00:00
2022-12-27 12:38:10 +00:00
@Published var statusText = NSMutableAttributedString(string: "") {
2022-12-25 05:55:33 +00:00
didSet {
highlightMeta()
2022-12-27 12:38:10 +00:00
checkEmbed()
2022-12-25 05:55:33 +00:00
}
}
2022-12-28 09:45:05 +00:00
@Published var spoilerOn: Bool = false
@Published var spoilerText: String = ""
2022-12-27 18:10:31 +00:00
@Published var selectedRange: NSRange = .init(location: 0, length: 0)
2022-12-25 16:46:51 +00:00
@Published var isPosting: Bool = false
2022-12-25 18:15:35 +00:00
@Published var selectedMedias: [PhotosPickerItem] = [] {
didSet {
2022-12-27 18:10:31 +00:00
if selectedMedias.count > 4 {
selectedMedias = selectedMedias.prefix(4).map{ $0 }
}
2022-12-25 18:15:35 +00:00
inflateSelectedMedias()
}
}
@Published var mediasImages: [ImageContainer] = []
2022-12-30 21:49:09 +00:00
@Published var replyToStatus: Status?
2022-12-27 12:38:10 +00:00
@Published var embededStatus: Status?
2022-12-27 15:16:25 +00:00
2022-12-27 18:10:31 +00:00
@Published var visibility: Models.Visibility = .pub
@Published var mentionsSuggestions: [Account] = []
@Published var tagsSuggestions: [Tag] = []
private var currentSuggestionRange: NSRange?
2022-12-30 11:00:09 +00:00
private var embededStatusURL: URL? {
return embededStatus?.reblog?.url ?? embededStatus?.url
}
2022-12-27 15:16:25 +00:00
private var uploadTask: Task<Void, Never>?
2022-12-27 12:38:10 +00:00
2022-12-26 07:24:55 +00:00
init(mode: Mode) {
self.mode = mode
2022-12-25 07:17:16 +00:00
}
2022-12-27 18:10:31 +00:00
func insertStatusText(text: String) {
let string = statusText
string.mutableString.insert(text, at: selectedRange.location)
statusText = string
selectedRange = NSRange(location: selectedRange.location + text.utf16.count, length: 0)
}
func replaceTextWith(text: String, inRange: NSRange) {
let string = statusText
string.mutableString.deleteCharacters(in: inRange)
string.mutableString.insert(text, at: inRange.location)
statusText = string
selectedRange = NSRange(location: inRange.location + text.utf16.count, length: 0)
}
2022-12-25 07:17:16 +00:00
func postStatus() async -> Status? {
guard let client else { return nil }
do {
2022-12-25 16:46:51 +00:00
isPosting = true
2022-12-26 07:24:55 +00:00
let postStatus: Status?
switch mode {
2023-01-04 17:37:58 +00:00
case .new, .replyTo, .quote, .mention:
2022-12-26 07:24:55 +00:00
postStatus = try await client.post(endpoint: Statuses.postStatus(status: statusText.string,
inReplyTo: mode.replyToStatus?.id,
2022-12-27 15:16:25 +00:00
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
2022-12-28 09:45:05 +00:00
spoilerText: spoilerOn ? spoilerText : nil,
2022-12-27 18:10:31 +00:00
visibility: visibility))
2022-12-26 07:24:55 +00:00
case let .edit(status):
postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id,
status: statusText.string,
2022-12-27 15:16:25 +00:00
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
2022-12-28 09:45:05 +00:00
spoilerText: spoilerOn ? spoilerText : nil,
2022-12-27 18:10:31 +00:00
visibility: visibility))
2022-12-26 07:24:55 +00:00
}
2022-12-25 16:46:51 +00:00
generator.notificationOccurred(.success)
isPosting = false
2022-12-26 07:24:55 +00:00
return postStatus
2022-12-25 07:17:16 +00:00
} catch {
2022-12-25 16:46:51 +00:00
isPosting = false
generator.notificationOccurred(.error)
2022-12-25 07:17:16 +00:00
return nil
}
}
2022-12-26 07:24:55 +00:00
func prepareStatusText() {
switch mode {
case let .new(visibility):
self.visibility = visibility
2022-12-26 07:24:55 +00:00
case let .replyTo(status):
var mentionString = ""
if (status.reblog?.account.acct ?? status.account.acct) != currentAccount?.acct {
mentionString = "@\(status.reblog?.account.acct ?? status.account.acct)"
}
2022-12-30 21:49:09 +00:00
for mention in status.mentions where mention.acct != currentAccount?.acct {
mentionString += " @\(mention.acct)"
}
mentionString += " "
replyToStatus = status
visibility = status.visibility
2022-12-30 21:49:09 +00:00
statusText = .init(string: mentionString)
selectedRange = .init(location: mentionString.utf16.count, length: 0)
2023-01-04 17:37:58 +00:00
case let .mention(account, visibility):
statusText = .init(string: "@\(account.acct) ")
self.visibility = visibility
selectedRange = .init(location: statusText.string.utf16.count, length: 0)
2022-12-26 07:24:55 +00:00
case let .edit(status):
2022-12-27 12:38:10 +00:00
statusText = .init(status.content.asSafeAttributedString)
2022-12-28 09:45:05 +00:00
selectedRange = .init(location: statusText.string.utf16.count, length: 0)
spoilerOn = !status.spoilerText.isEmpty
spoilerText = status.spoilerText
visibility = status.visibility
2022-12-28 09:45:05 +00:00
mediasImages = status.mediaAttachments.map{ .init(image: nil, mediaAttachement: $0, error: nil )}
2022-12-27 06:51:44 +00:00
case let .quote(status):
2022-12-27 12:38:10 +00:00
self.embededStatus = status
2022-12-30 11:00:09 +00:00
if let url = embededStatusURL {
2022-12-27 12:38:10 +00:00
statusText = .init(string: "\n\nFrom: @\(status.reblog?.account.acct ?? status.account.acct)\n\(url)")
2022-12-27 18:10:31 +00:00
selectedRange = .init(location: 0, length: 0)
2022-12-27 06:51:44 +00:00
}
2022-12-26 07:24:55 +00:00
default:
break
2022-12-25 07:17:16 +00:00
}
}
2022-12-25 05:55:33 +00:00
2022-12-27 12:38:10 +00:00
private func highlightMeta() {
statusText.addAttributes([.foregroundColor: UIColor(Color.label)],
range: NSMakeRange(0, statusText.string.utf16.count))
2022-12-25 05:55:33 +00:00
let hashtagPattern = "(#+[a-zA-Z0-9(_)]{1,})"
2022-12-25 07:17:16 +00:00
let mentionPattern = "(@+[a-zA-Z0-9(_).]{1,})"
2022-12-27 06:51:44 +00:00
let urlPattern = "(?i)https?://(?:www\\.)?\\S+(?:/|\\b)"
2022-12-25 05:55:33 +00:00
do {
let hashtagRegex = try NSRegularExpression(pattern: hashtagPattern, options: [])
let mentionRegex = try NSRegularExpression(pattern: mentionPattern, options: [])
2022-12-27 06:51:44 +00:00
let urlRegex = try NSRegularExpression(pattern: urlPattern, options: [])
2022-12-27 12:38:10 +00:00
var ranges = hashtagRegex.matches(in: statusText.string,
options: [],
2022-12-27 12:38:10 +00:00
range: NSMakeRange(0, statusText.string.utf16.count)).map { $0.range }
ranges.append(contentsOf: mentionRegex.matches(in: statusText.string,
options: [],
2022-12-27 12:38:10 +00:00
range: NSMakeRange(0, statusText.string.utf16.count)).map {$0.range})
2022-12-27 06:51:44 +00:00
2022-12-27 12:38:10 +00:00
let urlRanges = urlRegex.matches(in: statusText.string,
2022-12-27 06:51:44 +00:00
options: [],
2022-12-27 12:38:10 +00:00
range: NSMakeRange(0, statusText.string.utf16.count)).map { $0.range }
2022-12-25 05:55:33 +00:00
var foundSuggestionRange: Bool = false
for nsRange in ranges {
2022-12-31 11:11:42 +00:00
statusText.addAttributes([.foregroundColor: UIColor(theme?.tintColor ?? .brand)],
range: nsRange)
if selectedRange.location == (nsRange.location + nsRange.length),
let range = Range(nsRange, in: statusText.string) {
foundSuggestionRange = true
currentSuggestionRange = nsRange
loadAutoCompleteResults(query: String(statusText.string[range]))
}
}
if !foundSuggestionRange || ranges.isEmpty{
resetAutoCompletion()
}
2022-12-27 06:51:44 +00:00
for range in urlRanges {
2022-12-31 11:11:42 +00:00
statusText.addAttributes([.foregroundColor: UIColor(theme?.tintColor ?? .brand),
2022-12-27 06:51:44 +00:00
.underlineStyle: NSUnderlineStyle.single,
2022-12-31 11:11:42 +00:00
.underlineColor: UIColor(theme?.tintColor ?? .brand)],
2022-12-27 06:51:44 +00:00
range: NSRange(location: range.location, length: range.length))
}
} catch {
2022-12-25 05:55:33 +00:00
}
}
2022-12-25 18:15:35 +00:00
2022-12-27 12:38:10 +00:00
private func checkEmbed() {
2022-12-30 11:00:09 +00:00
if let url = embededStatusURL,
!statusText.string.contains(url.absoluteString) {
2022-12-27 12:38:10 +00:00
self.embededStatus = nil
self.mode = .new(vivibilty: visibility)
2022-12-27 12:38:10 +00:00
}
}
// MARK: - Autocomplete
private func loadAutoCompleteResults(query: String) {
guard let client, query.utf8.count > 1 else { return }
Task {
do {
var results: SearchResults?
switch query.first {
case "#":
results = try await client.get(endpoint: Search.search(query: query,
type: "hashtags",
offset: 0,
following: nil),
forceVersion: .v2)
withAnimation {
tagsSuggestions = results?.hashtags ?? []
}
case "@":
results = try await client.get(endpoint: Search.search(query: query,
type: "accounts",
offset: 0,
following: true),
forceVersion: .v2)
withAnimation {
mentionsSuggestions = results?.accounts ?? []
}
break
default:
break
}
} catch {
}
}
}
private func resetAutoCompletion() {
tagsSuggestions = []
mentionsSuggestions = []
currentSuggestionRange = nil
}
func selectMentionSuggestion(account: Account) {
if let range = currentSuggestionRange {
replaceTextWith(text: "@\(account.acct) ", inRange: range)
}
}
func selectHashtagSuggestion(tag: Tag) {
if let range = currentSuggestionRange {
replaceTextWith(text: "#\(tag.name) ", inRange: range)
}
}
2022-12-27 18:10:31 +00:00
// MARK: - Media related function
private func indexOf(container: ImageContainer) -> Int? {
mediasImages.firstIndex(where: { $0.id == container.id })
}
2022-12-25 18:15:35 +00:00
func inflateSelectedMedias() {
2022-12-27 15:16:25 +00:00
self.mediasImages = []
Task {
var medias: [ImageContainer] = []
for media in selectedMedias {
do {
if let data = try await media.loadTransferable(type: Data.self),
let image = UIImage(data: data) {
medias.append(.init(image: image, mediaAttachement: nil, error: nil))
}
} catch {
medias.append(.init(image: nil, mediaAttachement: nil, error: error))
}
}
DispatchQueue.main.async { [weak self] in
self?.mediasImages = medias
self?.processUpload()
}
}
}
private func processUpload() {
uploadTask?.cancel()
let mediasCopy = mediasImages
uploadTask = Task {
2022-12-27 18:10:31 +00:00
for media in mediasCopy {
if !Task.isCancelled {
await upload(container: media)
}
}
}
}
func upload(container: ImageContainer) async {
if let index = indexOf(container: container) {
let originalContainer = mediasImages[index]
let newContainer = ImageContainer(image: originalContainer.image, mediaAttachement: nil, error: nil)
mediasImages[index] = newContainer
do {
if let data = originalContainer.image?.jpegData(compressionQuality: 0.90) {
let uploadedMedia = try await uploadMedia(data: data)
if let index = indexOf(container: newContainer) {
2022-12-27 15:16:25 +00:00
mediasImages[index] = .init(image: nil, mediaAttachement: uploadedMedia, error: nil)
2022-12-25 18:15:35 +00:00
}
2022-12-27 18:10:31 +00:00
}
} catch {
if let index = indexOf(container: newContainer) {
mediasImages[index] = .init(image: originalContainer.image, mediaAttachement: nil, error: error)
2022-12-25 18:15:35 +00:00
}
}
}
}
2023-01-03 18:30:27 +00:00
func addDescription(container: ImageContainer, description: String) async {
guard let client, let attachment = container.mediaAttachement else { return }
if let index = indexOf(container: container) {
do {
let media: MediaAttachement = try await client.put(endpoint: Media.media(id: attachment.id,
description: description))
mediasImages[index] = .init(image: nil, mediaAttachement: media, error: nil)
} catch {
}
}
}
2022-12-25 05:55:33 +00:00
2022-12-27 15:16:25 +00:00
private func uploadMedia(data: Data) async throws -> MediaAttachement? {
guard let client else { return nil }
2022-12-27 18:10:31 +00:00
return try await client.mediaUpload(mimeType: "image/jpeg", data: data)
2022-12-27 15:16:25 +00:00
}
2022-12-25 05:55:33 +00:00
}