Compare commits
5 Commits
2cfc0cf28a
...
7883b04618
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 7883b04618 | |
Shadowfacts | 0687c040a0 | |
Shadowfacts | 58c6d508ec | |
Shadowfacts | ae272582ac | |
Shadowfacts | 1a4517c43a |
|
@ -18,6 +18,7 @@ public class Relationship: Decodable {
|
|||
public let followRequested: Bool
|
||||
public let domainBlocking: Bool
|
||||
public let showingReblogs: Bool
|
||||
public let endorsed: Bool?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
|
@ -29,5 +30,6 @@ public class Relationship: Decodable {
|
|||
case followRequested = "requested"
|
||||
case domainBlocking = "domain_blocking"
|
||||
case showingReblogs = "showing_reblogs"
|
||||
case endorsed
|
||||
}
|
||||
}
|
||||
|
|
|
@ -238,6 +238,7 @@
|
|||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; };
|
||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; };
|
||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
||||
D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */; };
|
||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
||||
|
@ -260,6 +261,7 @@
|
|||
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */; };
|
||||
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */; };
|
||||
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */; };
|
||||
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */; };
|
||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E0DC8D216EDF1E00369478 /* Previewing.swift */; };
|
||||
|
@ -566,6 +568,7 @@
|
|||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
|
||||
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; };
|
||||
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeContentWarningTextField.swift; sourceTree = "<group>"; };
|
||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||
|
@ -594,6 +597,7 @@
|
|||
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
||||
D6DD353E22F502EC00A9563A /* Preferences+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Notification.swift"; sourceTree = "<group>"; };
|
||||
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipMO.swift; sourceTree = "<group>"; };
|
||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakArray.swift; sourceTree = "<group>"; };
|
||||
D6E0DC8D216EDF1E00369478 /* Previewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Previewing.swift; sourceTree = "<group>"; };
|
||||
|
@ -893,6 +897,7 @@
|
|||
D6370B9A24421FF30092A7FF /* Tusker.xcdatamodeld */,
|
||||
D60E2F232442372B005F8713 /* StatusMO.swift */,
|
||||
D60E2F252442372B005F8713 /* AccountMO.swift */,
|
||||
D6DF95C02533F5DE0027A9B6 /* RelationshipMO.swift */,
|
||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
||||
);
|
||||
path = CoreData;
|
||||
|
@ -1001,6 +1006,7 @@
|
|||
D62275A524F1C81800B82A16 /* ComposeReplyView.swift */,
|
||||
D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */,
|
||||
D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */,
|
||||
D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */,
|
||||
);
|
||||
path = Compose;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1837,6 +1843,7 @@
|
|||
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
|
||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
||||
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
||||
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
|
||||
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
||||
D6D3FDE024F41B8400FF50A5 /* ComposeContainerView.swift in Sources */,
|
||||
|
@ -1856,6 +1863,7 @@
|
|||
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
|
||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||
D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */,
|
||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
||||
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
|
||||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
||||
|
|
|
@ -29,10 +29,13 @@ class FollowAccountActivity: AccountActivity {
|
|||
|
||||
let request = Account.follow(account.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case .failure(_) = response {
|
||||
switch response {
|
||||
case .failure(_):
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
case let .success(relationship, _):
|
||||
self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,10 +29,13 @@ class UnfollowAccountActivity: AccountActivity {
|
|||
|
||||
let request = Account.unfollow(account.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case .failure(_) = response {
|
||||
switch response {
|
||||
case .failure(_):
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
case let .success(relationship, _):
|
||||
self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
|||
|
||||
let statusSubject = PassthroughSubject<String, Never>()
|
||||
let accountSubject = PassthroughSubject<String, Never>()
|
||||
let relationshipSubject = PassthroughSubject<String, Never>()
|
||||
|
||||
init(for accountInfo: LocalData.UserAccountInfo?, transient: Bool = false) {
|
||||
if transient {
|
||||
|
@ -136,6 +137,40 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
|||
}
|
||||
}
|
||||
|
||||
func relationship(forAccount id: String, in context: NSManagedObjectContext? = nil) -> RelationshipMO? {
|
||||
let context = context ?? viewContext
|
||||
let request: NSFetchRequest<RelationshipMO> = RelationshipMO.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "accountID = %@", id)
|
||||
request.fetchLimit = 1
|
||||
if let result = try? context.fetch(request), let relationship = result.first {
|
||||
return relationship
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func upsert(relationship: Relationship) -> RelationshipMO {
|
||||
if let relationshipMO = self.relationship(forAccount: relationship.id, in: self.backgroundContext) {
|
||||
relationshipMO.updateFrom(apiRelationship: relationship, container: self)
|
||||
return relationshipMO
|
||||
} else {
|
||||
let relationshipMO = RelationshipMO(apiRelationship: relationship, container: self, context: self.backgroundContext)
|
||||
return relationshipMO
|
||||
}
|
||||
}
|
||||
|
||||
func addOrUpdate(relationship: Relationship, completion: ((RelationshipMO) -> Void)? = nil) {
|
||||
backgroundContext.perform {
|
||||
let relationshipMO = self.upsert(relationship: relationship)
|
||||
if self.backgroundContext.hasChanges {
|
||||
try! self.backgroundContext.save()
|
||||
}
|
||||
completion?(relationshipMO)
|
||||
self.relationshipSubject.send(relationship.id)
|
||||
}
|
||||
}
|
||||
|
||||
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
|
||||
backgroundContext.perform {
|
||||
accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
// RelationshipMO.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 10/11/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import Pachyderm
|
||||
|
||||
@objc(RelationshipMO)
|
||||
public final class RelationshipMO: NSManagedObject {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<RelationshipMO> {
|
||||
return NSFetchRequest<RelationshipMO>(entityName: "Relationship")
|
||||
}
|
||||
|
||||
@NSManaged public var accountID: String
|
||||
@NSManaged public var blocking: Bool
|
||||
@NSManaged public var domainBlocking: Bool
|
||||
@NSManaged public var endorsed: Bool
|
||||
@NSManaged public var followedBy: Bool
|
||||
@NSManaged public var following: Bool
|
||||
@NSManaged public var muting: Bool
|
||||
@NSManaged public var mutingNotifications: Bool
|
||||
@NSManaged public var requested: Bool
|
||||
@NSManaged public var showingReblogs: Bool
|
||||
@NSManaged public var account: AccountMO?
|
||||
|
||||
}
|
||||
|
||||
extension RelationshipMO {
|
||||
convenience init(apiRelationship relationship: Relationship, container: MastodonCachePersistentStore, context: NSManagedObjectContext) {
|
||||
self.init(context: context)
|
||||
self.updateFrom(apiRelationship: relationship, container: container)
|
||||
}
|
||||
|
||||
func updateFrom(apiRelationship relationship: Relationship, container: MastodonCachePersistentStore) {
|
||||
guard let context = managedObjectContext else {
|
||||
// we have been deleted, don't bother updating
|
||||
return
|
||||
}
|
||||
|
||||
self.accountID = relationship.id
|
||||
self.blocking = relationship.blocking
|
||||
self.domainBlocking = relationship.domainBlocking
|
||||
self.endorsed = relationship.endorsed ?? false
|
||||
self.followedBy = relationship.followedBy
|
||||
self.following = relationship.following
|
||||
self.muting = relationship.muting
|
||||
self.mutingNotifications = relationship.mutingNotifications
|
||||
self.requested = relationship.followRequested
|
||||
self.showingReblogs = relationship.showingReblogs
|
||||
|
||||
self.account = container.account(for: relationship.id, in: context)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19D76" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17507" systemVersion="19G2021" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="avatar" attributeType="URI"/>
|
||||
|
@ -20,12 +20,26 @@
|
|||
<attribute name="url" attributeType="URI"/>
|
||||
<attribute name="username" attributeType="String"/>
|
||||
<relationship name="movedTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
|
||||
<relationship name="relationship" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Relationship" inverseName="account" inverseEntity="Relationship"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Relationship" representedClassName="RelationshipMO" syncable="YES">
|
||||
<attribute name="accountID" optional="YES" attributeType="String"/>
|
||||
<attribute name="blocking" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="domainBlocking" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="endorsed" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="followedBy" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="following" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="muting" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mutingNotifications" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="requested" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="showingReblogs" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="relationship" inverseEntity="Account"/>
|
||||
</entity>
|
||||
<entity name="Status" representedClassName="StatusMO" syncable="YES">
|
||||
<attribute name="applicationName" optional="YES" attributeType="String"/>
|
||||
<attribute name="attachmentsData" attributeType="Binary"/>
|
||||
|
@ -59,7 +73,8 @@
|
|||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="328"/>
|
||||
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="343"/>
|
||||
<element name="Status" positionX="-63" positionY="-18" width="128" height="418"/>
|
||||
<element name="Relationship" positionX="63" positionY="135" width="128" height="208"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -73,7 +73,7 @@ struct ComposeAutocompleteMentionsView: View {
|
|||
HStack(spacing: 8) {
|
||||
ForEach(accounts, id: \.id) { (account) in
|
||||
Button {
|
||||
uiState.autocompleteHandler?.autocomplete(with: "@\(account.acct) ")
|
||||
uiState.autocompleteHandler?.autocomplete(with: "@\(account.acct)")
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
ComposeAvatarImageView(url: account.avatar)
|
||||
|
@ -210,7 +210,7 @@ struct ComposeAutocompleteEmojisView: View {
|
|||
HStack(spacing: 8) {
|
||||
ForEach(emojis, id: \.shortcode) { (emoji) in
|
||||
Button {
|
||||
uiState.autocompleteHandler?.autocomplete(with: ":\(emoji.shortcode): ")
|
||||
uiState.autocompleteHandler?.autocomplete(with: ":\(emoji.shortcode):")
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
CustomEmojiImageView(emoji: emoji)
|
||||
|
@ -266,7 +266,7 @@ struct ComposeAutocompleteHashtagsView: View {
|
|||
HStack(spacing: 8) {
|
||||
ForEach(hashtags, id: \.name) { (hashtag) in
|
||||
Button {
|
||||
uiState.autocompleteHandler?.autocomplete(with: "#\(hashtag.name) ")
|
||||
uiState.autocompleteHandler?.autocomplete(with: "#\(hashtag.name)")
|
||||
} label: {
|
||||
Text(verbatim: "#\(hashtag.name)")
|
||||
.foregroundColor(Color(UIColor.label))
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
//
|
||||
// ComposeContentWarningTextField.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 10/12/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeContentWarningTextField: UIViewRepresentable {
|
||||
typealias UIViewType = UITextField
|
||||
|
||||
@Binding var text: String
|
||||
|
||||
@EnvironmentObject private var uiState: ComposeUIState
|
||||
|
||||
func makeUIView(context: Context) -> UITextField {
|
||||
let view = UITextField()
|
||||
|
||||
view.placeholder = "Write your warning here"
|
||||
view.borderStyle = .roundedRect
|
||||
|
||||
view.delegate = context.coordinator
|
||||
view.addTarget(context.coordinator, action: #selector(Coordinator.didChange(_:)), for: .editingChanged)
|
||||
|
||||
context.coordinator.textField = view
|
||||
context.coordinator.uiState = uiState
|
||||
context.coordinator.text = $text
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextField, context: Context) {
|
||||
uiView.text = text
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
return Coordinator()
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UITextFieldDelegate, ComposeAutocompleteHandler {
|
||||
weak var textField: UITextField?
|
||||
var text: Binding<String>!
|
||||
var uiState: ComposeUIState!
|
||||
|
||||
@objc func didChange(_ textField: UITextField) {
|
||||
text.wrappedValue = textField.text ?? ""
|
||||
}
|
||||
|
||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
uiState.autocompleteHandler = self
|
||||
updateAutocompleteState(textField: textField)
|
||||
}
|
||||
|
||||
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
uiState.autocompleteHandler = nil
|
||||
updateAutocompleteState(textField: textField)
|
||||
}
|
||||
|
||||
func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||
updateAutocompleteState(textField: textField)
|
||||
}
|
||||
|
||||
func autocomplete(with string: String) {
|
||||
guard let textField = textField,
|
||||
let text = textField.text,
|
||||
let selectedRange = textField.selectedTextRange,
|
||||
let lastWordStartIndex = findAutocompleteLastWord(textField: textField) else {
|
||||
return
|
||||
}
|
||||
|
||||
let distanceToEnd = textField.offset(from: selectedRange.start, to: textField.endOfDocument)
|
||||
|
||||
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
|
||||
let characterBeforeCursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
|
||||
|
||||
let insertSpace: Bool
|
||||
if text.distance(from: characterBeforeCursorIndex, to: text.endIndex) > 0 {
|
||||
let charAfterCursor = text[text.index(after: characterBeforeCursorIndex)]
|
||||
insertSpace = charAfterCursor != " " && charAfterCursor != "\n"
|
||||
} else {
|
||||
insertSpace = true
|
||||
}
|
||||
let string = insertSpace ? string + " " : string
|
||||
|
||||
textField.text!.replaceSubrange(lastWordStartIndex..<characterBeforeCursorIndex, with: string)
|
||||
didChange(textField)
|
||||
|
||||
// keep the cursor at the same position in the text, immediately after what was inserted
|
||||
let newCursorPosition = textField.position(from: textField.endOfDocument, offset: -distanceToEnd)!
|
||||
textField.selectedTextRange = textField.textRange(from: newCursorPosition, to: newCursorPosition)
|
||||
}
|
||||
|
||||
private func updateAutocompleteState(textField: UITextField) {
|
||||
guard let selectedRange = textField.selectedTextRange,
|
||||
let text = textField.text,
|
||||
let lastWordStartIndex = findAutocompleteLastWord(textField: textField) else {
|
||||
uiState.autocompleteState = nil
|
||||
return
|
||||
}
|
||||
|
||||
if lastWordStartIndex > text.startIndex {
|
||||
let c = text[text.index(before: lastWordStartIndex)]
|
||||
if isPermittedForAutocomplete(c) || c == ":" {
|
||||
uiState.autocompleteState = nil
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
|
||||
let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
|
||||
|
||||
if lastWordStartIndex >= text.startIndex {
|
||||
let lastWord = text[lastWordStartIndex..<cursorIndex]
|
||||
let exceptFirst = lastWord[lastWord.index(after: lastWord.startIndex)...]
|
||||
|
||||
if lastWord.first == ":" {
|
||||
uiState.autocompleteState = .emoji(String(exceptFirst))
|
||||
} else {
|
||||
uiState.autocompleteState = nil
|
||||
}
|
||||
} else {
|
||||
uiState.autocompleteState = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func isPermittedForAutocomplete(_ c: Character) -> Bool {
|
||||
return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_"
|
||||
}
|
||||
|
||||
private func findAutocompleteLastWord(textField: UITextField) -> String.Index? {
|
||||
guard textField.isFirstResponder,
|
||||
let selectedRange = textField.selectedTextRange,
|
||||
selectedRange.isEmpty,
|
||||
let text = textField.text,
|
||||
!text.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
|
||||
let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
|
||||
|
||||
var lastWordStartIndex = text.index(before: cursorIndex)
|
||||
while true {
|
||||
let c = text[lastWordStartIndex]
|
||||
|
||||
if !isPermittedForAutocomplete(c) {
|
||||
break
|
||||
}
|
||||
|
||||
if lastWordStartIndex > text.startIndex {
|
||||
lastWordStartIndex = text.index(before: lastWordStartIndex)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return lastWordStartIndex
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -114,8 +114,7 @@ struct ComposeView: View {
|
|||
header
|
||||
|
||||
if draft.contentWarningEnabled {
|
||||
TextField("Write your warning here", text: $draft.contentWarning)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
ComposeContentWarningTextField(text: $draft.contentWarning)
|
||||
}
|
||||
|
||||
MainComposeTextView(
|
||||
|
@ -130,6 +129,7 @@ struct ComposeView: View {
|
|||
.padding([.top, .bottom], -8)
|
||||
}
|
||||
.padding(stackPadding)
|
||||
.padding(.bottom, uiState.autocompleteState != nil ? 46 : nil)
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
|
|
|
@ -218,11 +218,13 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
uiState.delegate?.keyboardDidHide(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
||||
}
|
||||
|
||||
func textViewDidEndEditing(_ textView: UITextView) {
|
||||
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
uiState.autocompleteHandler = self
|
||||
updateAutocompleteState()
|
||||
}
|
||||
|
||||
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
func textViewDidEndEditing(_ textView: UITextView) {
|
||||
uiState.autocompleteHandler = nil
|
||||
updateAutocompleteState()
|
||||
}
|
||||
|
||||
|
@ -237,10 +239,24 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
return
|
||||
}
|
||||
|
||||
let distanceToEnd = text.utf16.count - textView.selectedRange.upperBound
|
||||
|
||||
let characterBeforeCursorIndex = text.utf16.index(text.startIndex, offsetBy: textView.selectedRange.upperBound)
|
||||
|
||||
let insertSpace: Bool
|
||||
if text.distance(from: characterBeforeCursorIndex, to: text.endIndex) > 0 {
|
||||
let charAfterCursor = text[text.index(after: characterBeforeCursorIndex)]
|
||||
insertSpace = charAfterCursor != " " && charAfterCursor != "\n"
|
||||
} else {
|
||||
insertSpace = true
|
||||
}
|
||||
let string = insertSpace ? string + " " : string
|
||||
|
||||
textView.text.replaceSubrange(lastWordStartIndex..<characterBeforeCursorIndex, with: string)
|
||||
self.textViewDidChange(textView)
|
||||
|
||||
// keep the cursor at the same position in the text, immediately after what was inserted
|
||||
textView.selectedRange = NSRange(location: textView.text.utf16.count - distanceToEnd, length: 0)
|
||||
}
|
||||
|
||||
private func updateAutocompleteState() {
|
||||
|
@ -312,7 +328,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
var lastWordStartIndex = text.index(before: characterBeforeCursorIndex)
|
||||
var foundFirstAtSign = false
|
||||
while true {
|
||||
let c = textView.text[lastWordStartIndex]
|
||||
let c = text[lastWordStartIndex]
|
||||
|
||||
if !isPermittedForAutocomplete(c) {
|
||||
if foundFirstAtSign {
|
||||
|
|
|
@ -71,7 +71,13 @@ extension MenuPreviewProvider {
|
|||
elementHandler([
|
||||
self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in
|
||||
let request = (following ? Account.unfollow : Account.follow)(accountID)
|
||||
mastodonController.run(request) { (_) in
|
||||
mastodonController.run(request) { (response) in
|
||||
switch response {
|
||||
case .failure(_):
|
||||
fatalError()
|
||||
case let .success(relationship, _):
|
||||
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||
}
|
||||
}
|
||||
})
|
||||
])
|
||||
|
|
|
@ -23,7 +23,11 @@ class ProfileHeaderView: UIView {
|
|||
return nib.instantiate(withOwner: nil, options: nil).first as! ProfileHeaderView
|
||||
}
|
||||
|
||||
weak var delegate: ProfileHeaderViewDelegate?
|
||||
weak var delegate: ProfileHeaderViewDelegate? {
|
||||
didSet {
|
||||
createObservers()
|
||||
}
|
||||
}
|
||||
var mastodonController: MastodonController! { delegate?.apiController }
|
||||
|
||||
@IBOutlet weak var headerImageView: UIImageView!
|
||||
|
@ -41,10 +45,10 @@ class ProfileHeaderView: UIView {
|
|||
|
||||
var accountID: String!
|
||||
|
||||
var avatarRequest: ImageCache.Request?
|
||||
var headerRequest: ImageCache.Request?
|
||||
private var avatarRequest: ImageCache.Request?
|
||||
private var headerRequest: ImageCache.Request?
|
||||
|
||||
private var accountUpdater: Cancellable?
|
||||
private var cancellables = [AnyCancellable]()
|
||||
|
||||
deinit {
|
||||
avatarRequest?.cancel()
|
||||
|
@ -74,6 +78,22 @@ class ProfileHeaderView: UIView {
|
|||
}
|
||||
}
|
||||
|
||||
private func createObservers() {
|
||||
cancellables = []
|
||||
|
||||
mastodonController.persistentContainer.accountSubject
|
||||
.filter { [weak self] in $0 == self?.accountID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] in self?.updateUI(for: $0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
mastodonController.persistentContainer.relationshipSubject
|
||||
.filter { [weak self] in $0 == self?.accountID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] (_) in self?.updateRelationship() }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func updateUI(for accountID: String) {
|
||||
self.accountID = accountID
|
||||
|
||||
|
@ -111,6 +131,9 @@ class ProfileHeaderView: UIView {
|
|||
|
||||
// don't show relationship label for the user's own account
|
||||
if accountID != mastodonController.account?.id {
|
||||
// while fetching the most up-to-date, show the current data (if any)
|
||||
updateRelationship()
|
||||
|
||||
let request = Client.getRelationships(accounts: [accountID])
|
||||
mastodonController.run(request) { [weak self] (response) in
|
||||
guard let self = self,
|
||||
|
@ -118,9 +141,7 @@ class ProfileHeaderView: UIView {
|
|||
let relationship = results.first else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.followsYouLabel.isHidden = !relationship.followedBy
|
||||
}
|
||||
self.mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,13 +173,14 @@ class ProfileHeaderView: UIView {
|
|||
|
||||
nameLabel.heightAnchor.constraint(equalTo: valueTextView.heightAnchor).isActive = true
|
||||
}
|
||||
|
||||
if accountUpdater == nil {
|
||||
accountUpdater = mastodonController.persistentContainer.accountSubject
|
||||
.filter { [weak self] in $0 == self?.accountID }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] in self?.updateUI(for: $0) }
|
||||
}
|
||||
|
||||
private func updateRelationship() {
|
||||
guard let relationship = mastodonController.persistentContainer.relationship(forAccount: accountID) else {
|
||||
return
|
||||
}
|
||||
|
||||
followsYouLabel.isHidden = !relationship.followedBy
|
||||
}
|
||||
|
||||
@objc private func updateUIForPreferences() {
|
||||
|
|
Loading…
Reference in New Issue