Support local only posts on Hometown

Closes #130
This commit is contained in:
Shadowfacts 2022-01-23 23:44:38 -05:00
parent 072e68e97b
commit 02461ad46c
5 changed files with 86 additions and 5 deletions

View File

@ -335,7 +335,8 @@ public class Client {
language: String? = nil, language: String? = nil,
pollOptions: [String]? = nil, pollOptions: [String]? = nil,
pollExpiresIn: Int? = nil, pollExpiresIn: Int? = nil,
pollMultiple: Bool? = nil) -> Request<Status> { pollMultiple: Bool? = nil,
localOnly: Bool? = nil) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses", body: ParametersBody([ return Request<Status>(method: .post, path: "/api/v1/statuses", body: ParametersBody([
"status" => text, "status" => text,
"content_type" => contentType.mimeType, "content_type" => contentType.mimeType,
@ -346,6 +347,7 @@ public class Client {
"language" => language, "language" => language,
"poll[expires_in]" => pollExpiresIn, "poll[expires_in]" => pollExpiresIn,
"poll[multiple]" => pollMultiple, "poll[multiple]" => pollMultiple,
"local_only" => localOnly,
] + "media_ids" => media?.map { $0.id } + "poll[options]" => pollOptions)) ] + "media_ids" => media?.map { $0.id } + "poll[options]" => pollOptions))
} }

View File

@ -21,6 +21,7 @@ class Draft: Codable, ObservableObject {
@Published var inReplyToID: String? @Published var inReplyToID: String?
@Published var visibility: Status.Visibility @Published var visibility: Status.Visibility
@Published var poll: Poll? @Published var poll: Poll?
@Published var localOnly: Bool
var initialText: String var initialText: String
@ -49,6 +50,7 @@ class Draft: Codable, ObservableObject {
self.inReplyToID = nil self.inReplyToID = nil
self.visibility = Preferences.shared.defaultPostVisibility self.visibility = Preferences.shared.defaultPostVisibility
self.poll = nil self.poll = nil
self.localOnly = false
self.initialText = "" self.initialText = ""
} }
@ -67,6 +69,7 @@ class Draft: Codable, ObservableObject {
self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID) self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID)
self.visibility = try container.decode(Status.Visibility.self, forKey: .visibility) self.visibility = try container.decode(Status.Visibility.self, forKey: .visibility)
self.poll = try container.decode(Poll.self, forKey: .poll) self.poll = try container.decode(Poll.self, forKey: .poll)
self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly) ?? false
self.initialText = try container.decode(String.self, forKey: .initialText) self.initialText = try container.decode(String.self, forKey: .initialText)
} }
@ -85,6 +88,7 @@ class Draft: Codable, ObservableObject {
try container.encode(inReplyToID, forKey: .inReplyToID) try container.encode(inReplyToID, forKey: .inReplyToID)
try container.encode(visibility, forKey: .visibility) try container.encode(visibility, forKey: .visibility)
try container.encode(poll, forKey: .poll) try container.encode(poll, forKey: .poll)
try container.encode(localOnly, forKey: .localOnly)
try container.encode(initialText, forKey: .initialText) try container.encode(initialText, forKey: .initialText)
} }
@ -109,6 +113,7 @@ extension Draft {
case inReplyToID case inReplyToID
case visibility case visibility
case poll case poll
case localOnly
case initialText case initialText
} }

View File

@ -32,6 +32,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
private var mainToolbar: UIToolbar! private var mainToolbar: UIToolbar!
private var inputAccessoryToolbar: UIToolbar! private var inputAccessoryToolbar: UIToolbar!
private var visibilityBarButtonItems = [UIBarButtonItem]() private var visibilityBarButtonItems = [UIBarButtonItem]()
private var localOnlyItems = [UIBarButtonItem]()
override var inputAccessoryView: UIView? { inputAccessoryToolbar } override var inputAccessoryView: UIView? { inputAccessoryToolbar }
@ -54,6 +55,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
self.uiState.delegate = self self.uiState.delegate = self
// main toolbar is shown at the bottom of the screen, the input accessory is attached to the keyboard while editing // main toolbar is shown at the bottom of the screen, the input accessory is attached to the keyboard while editing
// (except for MainComposeTextView which has its own accessory to add formatting buttons)
mainToolbar = createToolbar() mainToolbar = createToolbar()
inputAccessoryToolbar = createToolbar() inputAccessoryToolbar = createToolbar()
@ -73,6 +75,11 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
.sink(receiveValue: self.visibilityChanged) .sink(receiveValue: self.visibilityChanged)
.store(in: &cancellables) .store(in: &cancellables)
self.uiState.$draft
.flatMap(\.$localOnly)
.sink(receiveValue: self.localOnlyChanged)
.store(in: &cancellables)
self.uiState.$draft self.uiState.$draft
.flatMap(\.objectWillChange) .flatMap(\.objectWillChange)
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.global(qos: .utility)) .debounce(for: .milliseconds(250), scheduler: DispatchQueue.global(qos: .utility))
@ -114,7 +121,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
toolbar.translatesAutoresizingMaskIntoConstraints = false toolbar.translatesAutoresizingMaskIntoConstraints = false
toolbar.isAccessibilityElement = true toolbar.isAccessibilityElement = true
let visibilityItem = UIBarButtonItem(image: UIImage(systemName: draft.visibility.imageName), style: .plain, target: nil, action: nil) let visibilityItem = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
visibilityBarButtonItems.append(visibilityItem) visibilityBarButtonItems.append(visibilityItem)
visibilityChanged(draft.visibility) visibilityChanged(draft.visibility)
@ -124,6 +131,14 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPresed)) UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPresed))
] ]
if mastodonController.instanceFeatures.localOnlyPosts {
let item = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
toolbar.items!.insert(item, at: 2)
localOnlyItems.append(item)
localOnlyChanged(draft.localOnly)
}
return toolbar return toolbar
} }
@ -185,11 +200,10 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
private func visibilityChanged(_ newVisibility: Status.Visibility) { private func visibilityChanged(_ newVisibility: Status.Visibility) {
for item in visibilityBarButtonItems { for item in visibilityBarButtonItems {
item.image = UIImage(systemName: newVisibility.imageName) item.image = UIImage(systemName: newVisibility.imageName)
item.image!.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName) item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
let state = visibility == newVisibility ? UIMenuElement.State.on : .off let state = visibility == newVisibility ? UIMenuElement.State.on : .off
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { (_) in return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { [unowned self] (_) in
self.draft.visibility = visibility self.draft.visibility = visibility
} }
} }
@ -197,6 +211,27 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
} }
} }
private func localOnlyChanged(_ localOnly: Bool) {
for item in localOnlyItems {
if localOnly {
item.image = UIImage(named: "link.broken")
item.accessibilityLabel = "Local-only"
} else {
item.image = UIImage(systemName: "link")
item.accessibilityLabel = "Federated"
}
item.menu = UIMenu(children: [
// todo: iOS 15, action subtitles
UIAction(title: "Local-only", image: UIImage(named: "link.broken"), state: localOnly ? .on : .off) { [unowned self] (_) in
self.draft.localOnly = true
},
UIAction(title: "Federated", image: UIImage(systemName: "link"), state: localOnly ? .off : .on) { [unowned self] (_) in
self.draft.localOnly = false
},
])
}
}
override func canPaste(_ itemProviders: [NSItemProvider]) -> Bool { override func canPaste(_ itemProviders: [NSItemProvider]) -> Bool {
guard itemProviders.allSatisfy({ $0.canLoadObject(ofClass: CompositionAttachment.self) }) else { return false } guard itemProviders.allSatisfy({ $0.canLoadObject(ofClass: CompositionAttachment.self) }) else { return false }
switch mastodonController.instance.instanceType { switch mastodonController.instance.instanceType {

View File

@ -219,7 +219,8 @@ struct ComposeView: View {
language: nil, language: nil,
pollOptions: draft.poll?.options.map(\.text), pollOptions: draft.poll?.options.map(\.text),
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration), pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
pollMultiple: draft.poll?.multiple) pollMultiple: draft.poll?.multiple,
localOnly: mastodonController.instanceFeatures.localOnlyPosts ? draft.localOnly : nil)
self.mastodonController.run(request) { (response) in self.mastodonController.run(request) { (response) in
switch response { switch response {
case let .failure(error): case let .failure(error):

View File

@ -56,7 +56,10 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
var textDidChange: (UITextView) -> Void var textDidChange: (UITextView) -> Void
@EnvironmentObject var uiState: ComposeUIState @EnvironmentObject var uiState: ComposeUIState
@EnvironmentObject var mastodonController: MastodonController
// todo: should these be part of the coordinator?
@State var visibilityButton: UIBarButtonItem? @State var visibilityButton: UIBarButtonItem?
@State var localOnlyButton: UIBarButtonItem?
func makeUIView(context: Context) -> UITextView { func makeUIView(context: Context) -> UITextView {
let textView = WrappedTextView() let textView = WrappedTextView()
@ -87,6 +90,22 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
self.visibilityButton = visibilityButton self.visibilityButton = visibilityButton
} }
if mastodonController.instanceFeatures.localOnlyPosts {
let image: UIImage
if uiState.draft.localOnly {
image = UIImage(named: "link.broken")!
} else {
image = UIImage(systemName: "link")!
}
let item = UIBarButtonItem(image: image, style: .plain, target: nil, action: nil)
toolbar.items!.insert(item, at: 2)
updateLocalOnlyMenu(item)
DispatchQueue.main.async {
self.localOnlyButton = item
}
}
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil) NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
@ -134,6 +153,17 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
visibilityButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements) visibilityButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: elements)
} }
private func updateLocalOnlyMenu(_ localOnlyButton: UIBarButtonItem) {
localOnlyButton.menu = UIMenu(children: [
UIAction(title: "Local-only", image: UIImage(named: "link.broken"), state: uiState.draft.localOnly ? .on : .off) { (_) in
self.uiState.draft.localOnly = true
},
UIAction(title: "Federated", image: UIImage(systemName: "link"), state: uiState.draft.localOnly ? .off : .on) { (_) in
self.uiState.draft.localOnly = false
},
])
}
func updateUIView(_ uiView: UITextView, context: Context) { func updateUIView(_ uiView: UITextView, context: Context) {
if context.coordinator.skipSettingTextOnNextUpdate { if context.coordinator.skipSettingTextOnNextUpdate {
context.coordinator.skipSettingTextOnNextUpdate = false context.coordinator.skipSettingTextOnNextUpdate = false
@ -145,6 +175,14 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
visibilityButton.image = UIImage(systemName: visibility.imageName) visibilityButton.image = UIImage(systemName: visibility.imageName)
updateVisibilityMenu(visibilityButton) updateVisibilityMenu(visibilityButton)
} }
if let localOnlyButton = localOnlyButton {
if uiState.draft.localOnly {
localOnlyButton.image = UIImage(named: "link.broken")
} else {
localOnlyButton.image = UIImage(systemName: "link")
}
updateLocalOnlyMenu(localOnlyButton)
}
context.coordinator.text = $text context.coordinator.text = $text
context.coordinator.didChange = textDidChange context.coordinator.didChange = textDidChange
context.coordinator.uiState = uiState context.coordinator.uiState = uiState