Compare commits
7 Commits
dc818524b2
...
bc9a700383
Author | SHA1 | Date |
---|---|---|
Shadowfacts | bc9a700383 | |
Shadowfacts | 62c7a30bbc | |
Shadowfacts | abf6ff8115 | |
Shadowfacts | a718721537 | |
Shadowfacts | 4f99d3c6e1 | |
Shadowfacts | a2fc1652d1 | |
Shadowfacts | 77007dcea0 |
|
@ -25,7 +25,7 @@ extension RequestRange {
|
|||
case let .before(id, count):
|
||||
return ["max_id" => id, "count" => count]
|
||||
case let .after(id, count):
|
||||
return ["min_id" => id, "count" => count]
|
||||
return ["since_id" => id, "count" => count]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,11 +34,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
let config = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all)
|
||||
AppDelegate.crashReporter = PLCrashReporter(configuration: config)
|
||||
|
||||
if AppDelegate.crashReporter.hasPendingCrashReport() {
|
||||
let data = try! AppDelegate.crashReporter.loadPendingCrashReportDataAndReturnError()
|
||||
if AppDelegate.crashReporter.hasPendingCrashReport(),
|
||||
let data = try? AppDelegate.crashReporter.loadPendingCrashReportDataAndReturnError(),
|
||||
let report = try? PLCrashReport(data: data) {
|
||||
AppDelegate.crashReporter.purgePendingCrashReport()
|
||||
let report = try! PLCrashReport(data: data)
|
||||
|
||||
AppDelegate.pendingCrashReport = report
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ class Preferences: Codable, ObservableObject {
|
|||
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
|
||||
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
||||
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
|
||||
self.hideActionsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .hideActionsInTimeline) ?? false
|
||||
|
||||
self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility)
|
||||
self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts)
|
||||
|
@ -81,6 +82,7 @@ class Preferences: Codable, ObservableObject {
|
|||
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
|
||||
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
|
||||
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
|
||||
try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline)
|
||||
|
||||
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
|
||||
try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts)
|
||||
|
@ -118,6 +120,7 @@ class Preferences: Codable, ObservableObject {
|
|||
@Published var hideCustomEmojiInUsernames = false
|
||||
@Published var showIsStatusReplyIcon = false
|
||||
@Published var alwaysShowStatusVisibilityIcon = false
|
||||
@Published var hideActionsInTimeline = false
|
||||
|
||||
// MARK: Composing
|
||||
@Published var defaultPostVisibility = Status.Visibility.public
|
||||
|
@ -160,6 +163,7 @@ class Preferences: Codable, ObservableObject {
|
|||
case hideCustomEmojiInUsernames
|
||||
case showIsStatusReplyIcon
|
||||
case alwaysShowStatusVisibilityIcon
|
||||
case hideActionsInTimeline
|
||||
|
||||
case defaultPostVisibility
|
||||
case automaticallySaveDrafts
|
||||
|
|
|
@ -60,7 +60,7 @@ struct ComposeAutocompleteMentionsView: View {
|
|||
HStack(spacing: 8) {
|
||||
ForEach(accounts, id: \.id) { (account) in
|
||||
Button {
|
||||
uiState.autocompleteHandler?.autocomplete(with: "@\(account.acct)")
|
||||
uiState.currentInput?.autocomplete(with: "@\(account.acct)")
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
ComposeAvatarImageView(url: account.avatar)
|
||||
|
@ -234,6 +234,12 @@ struct ComposeAutocompleteEmojisView: View {
|
|||
} else {
|
||||
horizontalScrollView
|
||||
.onReceive(uiState.$autocompleteState, perform: queryChanged)
|
||||
.onAppear {
|
||||
if uiState.shouldEmojiAutocompletionBeginExpanded {
|
||||
expanded = true
|
||||
uiState.shouldEmojiAutocompletionBeginExpanded = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,7 +248,7 @@ struct ComposeAutocompleteEmojisView: View {
|
|||
HStack(spacing: 8) {
|
||||
ForEach(emojis, id: \.shortcode) { (emoji) in
|
||||
Button {
|
||||
uiState.autocompleteHandler?.autocomplete(with: ":\(emoji.shortcode):")
|
||||
uiState.currentInput?.autocomplete(with: ":\(emoji.shortcode):")
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
CustomEmojiImageView(emoji: emoji)
|
||||
|
@ -306,7 +312,7 @@ struct ComposeAutocompleteHashtagsView: View {
|
|||
HStack(spacing: 8) {
|
||||
ForEach(hashtags, id: \.name) { (hashtag) in
|
||||
Button {
|
||||
uiState.autocompleteHandler?.autocomplete(with: "#\(hashtag.name)")
|
||||
uiState.currentInput?.autocomplete(with: "#\(hashtag.name)")
|
||||
} label: {
|
||||
Text(verbatim: "#\(hashtag.name)")
|
||||
.foregroundColor(Color(UIColor.label))
|
||||
|
|
|
@ -73,22 +73,27 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
|||
return Coordinator()
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UITextFieldDelegate, ComposeAutocompleteHandler {
|
||||
class Coordinator: NSObject, UITextFieldDelegate, ComposeInput {
|
||||
weak var textField: UITextField?
|
||||
var text: Binding<String>!
|
||||
var uiState: ComposeUIState!
|
||||
// break retained cycle through ComposeUIState.currentInput
|
||||
unowned var uiState: ComposeUIState!
|
||||
var didChange: ((String) -> Void)?
|
||||
var didEndEditing: (() -> Void)?
|
||||
|
||||
var skipSettingTextOnNextUpdate = false
|
||||
|
||||
var toolbarElements: [ComposeUIState.ToolbarElement] {
|
||||
[.emojiPicker]
|
||||
}
|
||||
|
||||
@objc func didChange(_ textField: UITextField) {
|
||||
text.wrappedValue = textField.text ?? ""
|
||||
didChange?(text.wrappedValue)
|
||||
}
|
||||
|
||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
uiState.autocompleteHandler = self
|
||||
uiState.currentInput = self
|
||||
updateAutocompleteState(textField: textField)
|
||||
}
|
||||
|
||||
|
@ -103,6 +108,13 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
|||
self.updateAutocompleteState(textField: textField)
|
||||
}
|
||||
|
||||
func beginAutocompletingEmoji() {
|
||||
textField?.insertText(":")
|
||||
}
|
||||
|
||||
func applyFormat(_ format: StatusFormat) {
|
||||
}
|
||||
|
||||
func autocomplete(with string: String) {
|
||||
guard let textField = textField,
|
||||
let text = textField.text,
|
||||
|
|
|
@ -15,6 +15,9 @@ protocol ComposeHostingControllerDelegate: AnyObject {
|
|||
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
|
||||
}
|
||||
|
||||
private let VISIBILITY_BAR_BUTTON_TAG = 42001
|
||||
private let LOCAL_ONLY_BAR_BUTTON_TAG = 42002
|
||||
|
||||
class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||
|
||||
weak var delegate: ComposeHostingControllerDelegate?
|
||||
|
@ -31,8 +34,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
|
||||
private var mainToolbar: UIToolbar!
|
||||
private var inputAccessoryToolbar: UIToolbar!
|
||||
private var visibilityBarButtonItems = [UIBarButtonItem]()
|
||||
private var localOnlyItems = [UIBarButtonItem]()
|
||||
|
||||
override var inputAccessoryView: UIView? { inputAccessoryToolbar }
|
||||
|
||||
|
@ -56,8 +57,14 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
|
||||
// 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()
|
||||
inputAccessoryToolbar = createToolbar()
|
||||
mainToolbar = UIToolbar()
|
||||
mainToolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
mainToolbar.isAccessibilityElement = true
|
||||
setupToolbarItems(toolbar: mainToolbar, input: nil)
|
||||
inputAccessoryToolbar = UIToolbar()
|
||||
inputAccessoryToolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
inputAccessoryToolbar.isAccessibilityElement = true
|
||||
setupToolbarItems(toolbar: inputAccessoryToolbar, input: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
|
@ -87,6 +94,12 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
DraftsManager.save()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
self.uiState.$currentInput
|
||||
.sink { [unowned self] in
|
||||
self.setupToolbarItems(toolbar: self.inputAccessoryToolbar, input: $0)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
@ -116,30 +129,61 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
DraftsManager.save()
|
||||
}
|
||||
|
||||
private func createToolbar() -> UIToolbar {
|
||||
let toolbar = UIToolbar()
|
||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
toolbar.isAccessibilityElement = true
|
||||
private func setupToolbarItems(toolbar: UIToolbar, input: ComposeInput?) {
|
||||
var items: [UIBarButtonItem] = []
|
||||
|
||||
items.append(UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)))
|
||||
|
||||
let visibilityItem = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
||||
visibilityBarButtonItems.append(visibilityItem)
|
||||
visibilityChanged(draft.visibility)
|
||||
|
||||
toolbar.items = [
|
||||
UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)),
|
||||
visibilityItem,
|
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
||||
UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPresed))
|
||||
]
|
||||
visibilityItem.tag = VISIBILITY_BAR_BUTTON_TAG
|
||||
items.append(visibilityItem)
|
||||
|
||||
if mastodonController.instanceFeatures.localOnlyPosts {
|
||||
let item = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
||||
toolbar.items!.insert(item, at: 2)
|
||||
localOnlyItems.append(item)
|
||||
item.tag = LOCAL_ONLY_BAR_BUTTON_TAG
|
||||
items.append(item)
|
||||
localOnlyChanged(draft.localOnly)
|
||||
}
|
||||
|
||||
return toolbar
|
||||
if input?.toolbarElements.contains(.emojiPicker) == true {
|
||||
items.append(UIBarButtonItem(image: UIImage(systemName: "face.smiling"), style: .plain, target: self, action: #selector(emojiPickerButtonPressed)))
|
||||
}
|
||||
|
||||
items.append(UIBarButtonItem(systemItem: .flexibleSpace))
|
||||
|
||||
if input?.toolbarElements.contains(.formattingButtons) == true,
|
||||
Preferences.shared.statusContentType != .plain {
|
||||
|
||||
for (idx, format) in StatusFormat.allCases.enumerated() {
|
||||
let item: UIBarButtonItem
|
||||
if let image = format.image {
|
||||
item = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(formatButtonPressed(_:)))
|
||||
} else if let (str, attributes) = format.title {
|
||||
item = UIBarButtonItem(title: str, style: .plain, target: self, action: #selector(formatButtonPressed(_:)))
|
||||
item.setTitleTextAttributes(attributes, for: .normal)
|
||||
item.setTitleTextAttributes(attributes, for: .highlighted)
|
||||
} else {
|
||||
fatalError("StatusFormat must have either image or title")
|
||||
}
|
||||
item.tag = StatusFormat.allCases.firstIndex(of: format)!
|
||||
item.accessibilityLabel = format.accessibilityLabel
|
||||
|
||||
items.append(item)
|
||||
if idx != StatusFormat.allCases.count - 1 {
|
||||
let spacer = UIBarButtonItem(systemItem: .fixedSpace)
|
||||
spacer.width = 8
|
||||
items.append(spacer)
|
||||
}
|
||||
}
|
||||
|
||||
items.append(UIBarButtonItem(systemItem: .flexibleSpace))
|
||||
}
|
||||
|
||||
items.append(UIBarButtonItem(title: "Drafts", style: .plain, target: self, action: #selector(draftsButtonPresed)))
|
||||
|
||||
toolbar.items = items
|
||||
visibilityChanged(draft.visibility)
|
||||
localOnlyChanged(draft.localOnly)
|
||||
}
|
||||
|
||||
private func updateAdditionalSafeAreaInsets() {
|
||||
|
@ -198,7 +242,10 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
}
|
||||
|
||||
private func visibilityChanged(_ newVisibility: Status.Visibility) {
|
||||
for item in visibilityBarButtonItems {
|
||||
for toolbar in [mainToolbar, inputAccessoryToolbar] {
|
||||
guard let item = toolbar?.items?.first(where: { $0.tag == VISIBILITY_BAR_BUTTON_TAG }) else {
|
||||
continue
|
||||
}
|
||||
item.image = UIImage(systemName: newVisibility.imageName)
|
||||
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
||||
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
||||
|
@ -212,7 +259,10 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
}
|
||||
|
||||
private func localOnlyChanged(_ localOnly: Bool) {
|
||||
for item in localOnlyItems {
|
||||
for toolbar in [mainToolbar, inputAccessoryToolbar] {
|
||||
guard let item = toolbar?.items?.first(where: { $0.tag == LOCAL_ONLY_BAR_BUTTON_TAG }) else {
|
||||
continue
|
||||
}
|
||||
if localOnly {
|
||||
item.image = UIImage(named: "link.broken")
|
||||
item.accessibilityLabel = "Local-only"
|
||||
|
@ -260,6 +310,19 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
|||
draft.contentWarningEnabled = !draft.contentWarningEnabled
|
||||
}
|
||||
|
||||
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
||||
let format = StatusFormat.allCases[sender.tag]
|
||||
uiState.currentInput?.applyFormat(format)
|
||||
}
|
||||
|
||||
@objc func emojiPickerButtonPressed() {
|
||||
guard uiState.autocompleteState == nil else {
|
||||
return
|
||||
}
|
||||
uiState.shouldEmojiAutocompletionBeginExpanded = true
|
||||
uiState.currentInput?.beginAutocompletingEmoji()
|
||||
}
|
||||
|
||||
@objc func draftsButtonPresed() {
|
||||
let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!, exclude: draft)
|
||||
draftsVC.delegate = self
|
||||
|
|
|
@ -31,7 +31,8 @@ class ComposeUIState: ObservableObject {
|
|||
|
||||
var composeDrawingMode: ComposeDrawingMode?
|
||||
|
||||
weak var autocompleteHandler: ComposeAutocompleteHandler?
|
||||
var shouldEmojiAutocompletionBeginExpanded = false
|
||||
@Published var currentInput: ComposeInput?
|
||||
|
||||
init(draft: Draft) {
|
||||
self.draft = draft
|
||||
|
@ -60,6 +61,19 @@ extension ComposeUIState {
|
|||
}
|
||||
}
|
||||
|
||||
protocol ComposeAutocompleteHandler: AnyObject {
|
||||
protocol ComposeInput: AnyObject {
|
||||
var toolbarElements: [ComposeUIState.ToolbarElement] { get }
|
||||
|
||||
func autocomplete(with string: String)
|
||||
|
||||
func applyFormat(_ format: StatusFormat)
|
||||
|
||||
func beginAutocompletingEmoji()
|
||||
}
|
||||
|
||||
extension ComposeUIState {
|
||||
enum ToolbarElement {
|
||||
case emojiPicker
|
||||
case formattingButtons
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,18 +37,21 @@ class EmojiPickerCollectionViewController: UICollectionViewController {
|
|||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
let itemWidth = NSCollectionLayoutDimension.fractionalWidth(1.0 / 10)
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: itemWidth, heightDimension: itemWidth)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemWidth)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||
group.interItemSpacing = .fixed(4)
|
||||
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.interGroupSpacing = 4
|
||||
|
||||
let layout = UICollectionViewCompositionalLayout(section: section)
|
||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||
let hSizeClass = environment.traitCollection.horizontalSizeClass
|
||||
|
||||
let itemWidth = NSCollectionLayoutDimension.fractionalWidth(1.0 / (hSizeClass == .compact ? 10 : 20))
|
||||
let itemSize = NSCollectionLayoutSize(widthDimension: itemWidth, heightDimension: itemWidth)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
|
||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemWidth)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||
group.interItemSpacing = .fixed(4)
|
||||
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
section.interGroupSpacing = 4
|
||||
return section
|
||||
}
|
||||
|
||||
super.init(collectionViewLayout: layout)
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ struct EmojiPickerWrapper: UIViewControllerRepresentable {
|
|||
}
|
||||
|
||||
func selectedEmoji(_ emoji: Emoji) {
|
||||
uiState.autocompleteHandler?.autocomplete(with: ":\(emoji.shortcode):")
|
||||
uiState.currentInput?.autocomplete(with: ":\(emoji.shortcode):")
|
||||
uiState.autocompleteState = nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,9 +57,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
|
||||
@EnvironmentObject var uiState: ComposeUIState
|
||||
@EnvironmentObject var mastodonController: MastodonController
|
||||
// todo: should these be part of the coordinator?
|
||||
@State var visibilityButton: UIBarButtonItem?
|
||||
@State var localOnlyButton: UIBarButtonItem?
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let textView = WrappedTextView()
|
||||
|
@ -69,101 +66,9 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
textView.font = .systemFont(ofSize: 20)
|
||||
textView.textContainer.lineBreakMode = .byWordWrapping
|
||||
context.coordinator.textView = textView
|
||||
|
||||
uiState.autocompleteHandler = context.coordinator
|
||||
|
||||
let visibilityButton = UIBarButtonItem(image: UIImage(systemName: visibility.imageName), style: .plain, target: nil, action: nil)
|
||||
updateVisibilityMenu(visibilityButton)
|
||||
let toolbar = UIToolbar()
|
||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
toolbar.items = [
|
||||
UIBarButtonItem(title: "CW", style: .plain, target: nil, action: #selector(ComposeHostingController.cwButtonPressed)),
|
||||
visibilityButton,
|
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
||||
] + createFormattingButtons(coordinator: context.coordinator) + [
|
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
||||
UIBarButtonItem(title: "Drafts", style: .plain, target: nil, action: #selector(ComposeHostingController.draftsButtonPresed)),
|
||||
]
|
||||
textView.inputAccessoryView = toolbar
|
||||
// can't modify @State during view update
|
||||
DispatchQueue.main.async {
|
||||
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.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
private func createFormattingButtons(coordinator: Coordinator) -> [UIBarButtonItem] {
|
||||
guard Preferences.shared.statusContentType != .plain else {
|
||||
return []
|
||||
}
|
||||
|
||||
var formatButtons = StatusFormat.allCases.map { (format) -> UIBarButtonItem in
|
||||
let item: UIBarButtonItem
|
||||
if let image = format.image {
|
||||
item = UIBarButtonItem(image: image, style: .plain, target: coordinator, action: #selector(Coordinator.formatButtonPressed(_:)))
|
||||
} else if let (str, attributes) = format.title {
|
||||
item = UIBarButtonItem(title: str, style: .plain, target: coordinator, action: #selector(Coordinator.formatButtonPressed(_:)))
|
||||
item.setTitleTextAttributes(attributes, for: .normal)
|
||||
item.setTitleTextAttributes(attributes, for: .highlighted)
|
||||
} else {
|
||||
fatalError("StatusFormat must have either an image or a title")
|
||||
}
|
||||
item.tag = StatusFormat.allCases.firstIndex(of: format)!
|
||||
item.accessibilityLabel = format.accessibilityLabel
|
||||
return item
|
||||
}
|
||||
|
||||
for i in (1..<StatusFormat.allCases.count).reversed() {
|
||||
let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
|
||||
spacer.width = 8
|
||||
formatButtons.insert(spacer, at: i)
|
||||
}
|
||||
|
||||
return formatButtons
|
||||
}
|
||||
|
||||
private func updateVisibilityMenu(_ visibilityButton: UIBarButtonItem) {
|
||||
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
||||
let state = visibility == self.visibility ? UIMenuElement.State.on : .off
|
||||
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { (_) in
|
||||
self.uiState.draft.visibility = visibility
|
||||
}
|
||||
}
|
||||
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) {
|
||||
if context.coordinator.skipSettingTextOnNextUpdate {
|
||||
context.coordinator.skipSettingTextOnNextUpdate = false
|
||||
|
@ -171,18 +76,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
uiView.text = text
|
||||
}
|
||||
|
||||
if let visibilityButton = visibilityButton {
|
||||
visibilityButton.image = UIImage(systemName: visibility.imageName)
|
||||
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.didChange = textDidChange
|
||||
context.coordinator.uiState = uiState
|
||||
|
@ -232,17 +125,23 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
command.attributes.remove(.disabled)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UITextViewDelegate, ComposeAutocompleteHandler, ComposeTextViewCaretScrolling {
|
||||
class Coordinator: NSObject, UITextViewDelegate, ComposeInput, ComposeTextViewCaretScrolling {
|
||||
weak var textView: UITextView?
|
||||
var text: Binding<String>
|
||||
var didChange: (UITextView) -> Void
|
||||
var uiState: ComposeUIState
|
||||
// break retained cycle through ComposeUIState.currentInput
|
||||
unowned var uiState: ComposeUIState
|
||||
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||
|
||||
var skipSettingTextOnNextUpdate = false
|
||||
|
||||
var toolbarElements: [ComposeUIState.ToolbarElement] {
|
||||
[.emojiPicker, .formattingButtons]
|
||||
}
|
||||
|
||||
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
||||
self.text = text
|
||||
self.didChange = didChange
|
||||
|
@ -256,11 +155,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
ensureCursorVisible(textView: textView)
|
||||
}
|
||||
|
||||
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
||||
let format = StatusFormat.allCases[sender.tag]
|
||||
applyFormat(format)
|
||||
}
|
||||
|
||||
func applyFormat(_ format: StatusFormat) {
|
||||
guard let textView = textView,
|
||||
textView.isFirstResponder,
|
||||
|
@ -281,24 +175,13 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
}
|
||||
}
|
||||
|
||||
@objc func keyboardWillShow(_ notification: Foundation.Notification) {
|
||||
uiState.delegate?.keyboardWillShow(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
||||
}
|
||||
|
||||
@objc func keyboardWillHide(_ notification: Foundation.Notification) {
|
||||
uiState.delegate?.keyboardWillHide(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
||||
}
|
||||
|
||||
@objc func keyboardDidHide(_ notification: Foundation.Notification) {
|
||||
uiState.delegate?.keyboardDidHide(accessoryView: textView!.inputAccessoryView!, notification: notification)
|
||||
}
|
||||
|
||||
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
uiState.autocompleteHandler = self
|
||||
uiState.currentInput = self
|
||||
updateAutocompleteState()
|
||||
}
|
||||
|
||||
func textViewDidEndEditing(_ textView: UITextView) {
|
||||
uiState.currentInput = nil
|
||||
updateAutocompleteState()
|
||||
}
|
||||
|
||||
|
@ -310,6 +193,10 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
self.updateAutocompleteState()
|
||||
}
|
||||
|
||||
func beginAutocompletingEmoji() {
|
||||
textView?.insertText(":")
|
||||
}
|
||||
|
||||
func autocomplete(with string: String) {
|
||||
guard let textView = textView,
|
||||
let text = textView.text,
|
||||
|
|
|
@ -46,8 +46,6 @@ class MainSplitViewController: UISplitViewController {
|
|||
|
||||
preferredDisplayMode = .oneBesideSecondary
|
||||
preferredSplitBehavior = .tile
|
||||
presentsWithGesture = false
|
||||
showsSecondaryOnlyButton = false
|
||||
delegate = self
|
||||
|
||||
sidebar = MainSidebarViewController(mastodonController: mastodonController)
|
||||
|
|
|
@ -56,6 +56,9 @@ struct AppearancePrefsView : View {
|
|||
Toggle(isOn: $preferences.alwaysShowStatusVisibilityIcon) {
|
||||
Text("Always Show Status Visibility Icons")
|
||||
}
|
||||
Toggle(isOn: $preferences.hideActionsInTimeline) {
|
||||
Text("Hide Actions on Timeline")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,10 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
@IBOutlet weak var reblogLabel: EmojiLabel!
|
||||
@IBOutlet weak var timestampLabel: UILabel!
|
||||
@IBOutlet weak var pinImageView: UIImageView!
|
||||
@IBOutlet weak var actionsContainerView: UIView!
|
||||
@IBOutlet weak var actionsContainerHeightConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var verticalStackToActionsContainerConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var verticalStackToSuperviewConstraint: NSLayoutConstraint!
|
||||
|
||||
var reblogStatusID: String?
|
||||
var rebloggerID: String?
|
||||
|
@ -51,6 +55,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
contentTextView.defaultFont = .systemFont(ofSize: 16)
|
||||
|
||||
avatarImageView.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
|
||||
updateActionsVisibility()
|
||||
}
|
||||
|
||||
override func createObserversIfNecessary() {
|
||||
|
@ -121,9 +127,31 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
} else {
|
||||
metaIndicatorsView.allowedIndicators = .all.subtracting(.reply)
|
||||
}
|
||||
|
||||
let oldState = actionsContainerView.isHidden
|
||||
if oldState != Preferences.shared.hideActionsInTimeline {
|
||||
updateActionsVisibility()
|
||||
// not really accurate, but it notifies the vc our height has changed
|
||||
delegate?.statusCellCollapsedStateChanged(self)
|
||||
}
|
||||
|
||||
super.updateStatusIconsForPreferences(status)
|
||||
}
|
||||
|
||||
private func updateActionsVisibility() {
|
||||
if Preferences.shared.hideActionsInTimeline {
|
||||
actionsContainerView.isHidden = true
|
||||
actionsContainerHeightConstraint.isActive = false
|
||||
verticalStackToSuperviewConstraint.isActive = true
|
||||
verticalStackToActionsContainerConstraint.isActive = false
|
||||
} else {
|
||||
actionsContainerView.isHidden = false
|
||||
actionsContainerHeightConstraint.isActive = true
|
||||
verticalStackToSuperviewConstraint.isActive = false
|
||||
verticalStackToActionsContainerConstraint.isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTimestamp() {
|
||||
// if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated
|
||||
// so we bail out immediately, since there's nothing to update
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
|
@ -218,11 +218,17 @@
|
|||
<constraint firstItem="TUP-Nz-5Yh" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="qBn-Gk-DCa" secondAttribute="bottom" id="gxb-hp-7lU"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="gIY-Wp-RSk" secondAttribute="trailing" id="hKk-kO-wFT"/>
|
||||
<constraint firstItem="qBn-Gk-DCa" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="iLD-VU-ixJ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="gIY-Wp-RSk" secondAttribute="bottom" id="kq7-bk-S8j"/>
|
||||
<constraint firstAttribute="bottom" secondItem="TUP-Nz-5Yh" secondAttribute="bottom" id="rmQ-QM-Llu"/>
|
||||
<constraint firstItem="qBn-Gk-DCa" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="tKU-VP-n8P"/>
|
||||
<constraint firstItem="qBn-Gk-DCa" firstAttribute="width" secondItem="QMP-j2-HLn" secondAttribute="width" id="v1v-Pp-ubE"/>
|
||||
<constraint firstItem="QMP-j2-HLn" firstAttribute="leading" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="zeW-tQ-uJl"/>
|
||||
</constraints>
|
||||
<variation key="default">
|
||||
<mask key="constraints">
|
||||
<exclude reference="kq7-bk-S8j"/>
|
||||
</mask>
|
||||
</variation>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
|
@ -240,6 +246,8 @@
|
|||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="actionsContainerHeightConstraint" destination="1FK-Er-G11" id="9yN-NJ-GjN"/>
|
||||
<outlet property="actionsContainerView" destination="TUP-Nz-5Yh" id="NHM-5L-Odz"/>
|
||||
<outlet property="attachmentsView" destination="nbq-yr-2mA" id="SVm-zl-mPb"/>
|
||||
<outlet property="avatarImageView" destination="QMP-j2-HLn" id="xfS-v8-Gzu"/>
|
||||
<outlet property="cardView" destination="LKo-VB-XWl" id="6X5-8P-Ata"/>
|
||||
|
@ -257,6 +265,8 @@
|
|||
<outlet property="replyButton" destination="rKF-yF-KIa" id="rka-q1-o4a"/>
|
||||
<outlet property="timestampLabel" destination="35d-EA-ReR" id="Ny2-nV-nqP"/>
|
||||
<outlet property="usernameLabel" destination="j89-zc-SFa" id="bXX-FZ-fCp"/>
|
||||
<outlet property="verticalStackToActionsContainerConstraint" destination="4KL-a3-qyf" id="ooW-DI-8AX"/>
|
||||
<outlet property="verticalStackToSuperviewConstraint" destination="kq7-bk-S8j" id="Rfv-Bn-8B4"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="29.600000000000001" y="79.160419790104953"/>
|
||||
</view>
|
||||
|
|
Loading…
Reference in New Issue