Compare commits
12 Commits
dc818524b2
...
8c888906c9
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 8c888906c9 | |
Shadowfacts | d611aeb035 | |
Shadowfacts | 0e888d35eb | |
Shadowfacts | 98bb230817 | |
Shadowfacts | 3d6d9b2a91 | |
Shadowfacts | bc9a700383 | |
Shadowfacts | 62c7a30bbc | |
Shadowfacts | abf6ff8115 | |
Shadowfacts | a718721537 | |
Shadowfacts | 4f99d3c6e1 | |
Shadowfacts | a2fc1652d1 | |
Shadowfacts | 77007dcea0 |
|
@ -1,5 +1,14 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2022.1 (27)
|
||||||
|
Features/Improvements:
|
||||||
|
- Add emoji picker button to Compose screen toolbar
|
||||||
|
- Preference to disable reply/like/reblog/more buttons on statuses in the timeline
|
||||||
|
- iPad: Sidebar toggle button
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix crash when displaying malformed statuses
|
||||||
|
|
||||||
## 2022.1 (26)
|
## 2022.1 (26)
|
||||||
This build contains hotfixes for several crashes that may occur when logged-in to a Pixelfed account.
|
This build contains hotfixes for several crashes that may occur when logged-in to a Pixelfed account.
|
||||||
|
|
||||||
|
|
|
@ -2165,7 +2165,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 26;
|
CURRENT_PROJECT_VERSION = 27;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2196,7 +2196,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 26;
|
CURRENT_PROJECT_VERSION = 27;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2306,7 +2306,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 26;
|
CURRENT_PROJECT_VERSION = 27;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2333,7 +2333,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 26;
|
CURRENT_PROJECT_VERSION = 27;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
|
|
@ -34,11 +34,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
let config = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all)
|
let config = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all)
|
||||||
AppDelegate.crashReporter = PLCrashReporter(configuration: config)
|
AppDelegate.crashReporter = PLCrashReporter(configuration: config)
|
||||||
|
|
||||||
if AppDelegate.crashReporter.hasPendingCrashReport() {
|
if AppDelegate.crashReporter.hasPendingCrashReport(),
|
||||||
let data = try! AppDelegate.crashReporter.loadPendingCrashReportDataAndReturnError()
|
let data = try? AppDelegate.crashReporter.loadPendingCrashReportDataAndReturnError(),
|
||||||
|
let report = try? PLCrashReport(data: data) {
|
||||||
AppDelegate.crashReporter.purgePendingCrashReport()
|
AppDelegate.crashReporter.purgePendingCrashReport()
|
||||||
let report = try! PLCrashReport(data: data)
|
|
||||||
|
|
||||||
AppDelegate.pendingCrashReport = report
|
AppDelegate.pendingCrashReport = report
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
|
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
|
||||||
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
||||||
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
|
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.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility)
|
||||||
self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts)
|
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(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
|
||||||
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
|
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
|
||||||
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
|
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
|
||||||
|
try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline)
|
||||||
|
|
||||||
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
|
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
|
||||||
try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts)
|
try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts)
|
||||||
|
@ -118,6 +120,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var hideCustomEmojiInUsernames = false
|
@Published var hideCustomEmojiInUsernames = false
|
||||||
@Published var showIsStatusReplyIcon = false
|
@Published var showIsStatusReplyIcon = false
|
||||||
@Published var alwaysShowStatusVisibilityIcon = false
|
@Published var alwaysShowStatusVisibilityIcon = false
|
||||||
|
@Published var hideActionsInTimeline = false
|
||||||
|
|
||||||
// MARK: Composing
|
// MARK: Composing
|
||||||
@Published var defaultPostVisibility = Status.Visibility.public
|
@Published var defaultPostVisibility = Status.Visibility.public
|
||||||
|
@ -160,6 +163,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
case hideCustomEmojiInUsernames
|
case hideCustomEmojiInUsernames
|
||||||
case showIsStatusReplyIcon
|
case showIsStatusReplyIcon
|
||||||
case alwaysShowStatusVisibilityIcon
|
case alwaysShowStatusVisibilityIcon
|
||||||
|
case hideActionsInTimeline
|
||||||
|
|
||||||
case defaultPostVisibility
|
case defaultPostVisibility
|
||||||
case automaticallySaveDrafts
|
case automaticallySaveDrafts
|
||||||
|
|
|
@ -60,7 +60,7 @@ struct ComposeAutocompleteMentionsView: View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(accounts, id: \.id) { (account) in
|
ForEach(accounts, id: \.id) { (account) in
|
||||||
Button {
|
Button {
|
||||||
uiState.autocompleteHandler?.autocomplete(with: "@\(account.acct)")
|
uiState.currentInput?.autocomplete(with: "@\(account.acct)")
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
ComposeAvatarImageView(url: account.avatar)
|
ComposeAvatarImageView(url: account.avatar)
|
||||||
|
@ -234,6 +234,12 @@ struct ComposeAutocompleteEmojisView: View {
|
||||||
} else {
|
} else {
|
||||||
horizontalScrollView
|
horizontalScrollView
|
||||||
.onReceive(uiState.$autocompleteState, perform: queryChanged)
|
.onReceive(uiState.$autocompleteState, perform: queryChanged)
|
||||||
|
.onAppear {
|
||||||
|
if uiState.shouldEmojiAutocompletionBeginExpanded {
|
||||||
|
expanded = true
|
||||||
|
uiState.shouldEmojiAutocompletionBeginExpanded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,7 +248,7 @@ struct ComposeAutocompleteEmojisView: View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(emojis, id: \.shortcode) { (emoji) in
|
ForEach(emojis, id: \.shortcode) { (emoji) in
|
||||||
Button {
|
Button {
|
||||||
uiState.autocompleteHandler?.autocomplete(with: ":\(emoji.shortcode):")
|
uiState.currentInput?.autocomplete(with: ":\(emoji.shortcode):")
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
CustomEmojiImageView(emoji: emoji)
|
CustomEmojiImageView(emoji: emoji)
|
||||||
|
@ -306,7 +312,7 @@ struct ComposeAutocompleteHashtagsView: View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(hashtags, id: \.name) { (hashtag) in
|
ForEach(hashtags, id: \.name) { (hashtag) in
|
||||||
Button {
|
Button {
|
||||||
uiState.autocompleteHandler?.autocomplete(with: "#\(hashtag.name)")
|
uiState.currentInput?.autocomplete(with: "#\(hashtag.name)")
|
||||||
} label: {
|
} label: {
|
||||||
Text(verbatim: "#\(hashtag.name)")
|
Text(verbatim: "#\(hashtag.name)")
|
||||||
.foregroundColor(Color(UIColor.label))
|
.foregroundColor(Color(UIColor.label))
|
||||||
|
|
|
@ -73,22 +73,27 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
return Coordinator()
|
return Coordinator()
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, UITextFieldDelegate, ComposeAutocompleteHandler {
|
class Coordinator: NSObject, UITextFieldDelegate, ComposeInput {
|
||||||
weak var textField: UITextField?
|
weak var textField: UITextField?
|
||||||
var text: Binding<String>!
|
var text: Binding<String>!
|
||||||
var uiState: ComposeUIState!
|
// break retained cycle through ComposeUIState.currentInput
|
||||||
|
unowned var uiState: ComposeUIState!
|
||||||
var didChange: ((String) -> Void)?
|
var didChange: ((String) -> Void)?
|
||||||
var didEndEditing: (() -> Void)?
|
var didEndEditing: (() -> Void)?
|
||||||
|
|
||||||
var skipSettingTextOnNextUpdate = false
|
var skipSettingTextOnNextUpdate = false
|
||||||
|
|
||||||
|
var toolbarElements: [ComposeUIState.ToolbarElement] {
|
||||||
|
[.emojiPicker]
|
||||||
|
}
|
||||||
|
|
||||||
@objc func didChange(_ textField: UITextField) {
|
@objc func didChange(_ textField: UITextField) {
|
||||||
text.wrappedValue = textField.text ?? ""
|
text.wrappedValue = textField.text ?? ""
|
||||||
didChange?(text.wrappedValue)
|
didChange?(text.wrappedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||||
uiState.autocompleteHandler = self
|
uiState.currentInput = self
|
||||||
updateAutocompleteState(textField: textField)
|
updateAutocompleteState(textField: textField)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +108,13 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
self.updateAutocompleteState(textField: textField)
|
self.updateAutocompleteState(textField: textField)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func beginAutocompletingEmoji() {
|
||||||
|
textField?.insertText(":")
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyFormat(_ format: StatusFormat) {
|
||||||
|
}
|
||||||
|
|
||||||
func autocomplete(with string: String) {
|
func autocomplete(with string: String) {
|
||||||
guard let textField = textField,
|
guard let textField = textField,
|
||||||
let text = textField.text,
|
let text = textField.text,
|
||||||
|
|
|
@ -15,6 +15,9 @@ protocol ComposeHostingControllerDelegate: AnyObject {
|
||||||
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
|
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> {
|
class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
|
|
||||||
weak var delegate: ComposeHostingControllerDelegate?
|
weak var delegate: ComposeHostingControllerDelegate?
|
||||||
|
@ -31,8 +34,6 @@ 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 localOnlyItems = [UIBarButtonItem]()
|
|
||||||
|
|
||||||
override var inputAccessoryView: UIView? { inputAccessoryToolbar }
|
override var inputAccessoryView: UIView? { inputAccessoryToolbar }
|
||||||
|
|
||||||
|
@ -56,12 +57,18 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
|
|
||||||
// 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)
|
// (except for MainComposeTextView which has its own accessory to add formatting buttons)
|
||||||
mainToolbar = createToolbar()
|
mainToolbar = UIToolbar()
|
||||||
inputAccessoryToolbar = createToolbar()
|
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(composeKeyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(composeKeyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(composeKeyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil)
|
||||||
|
|
||||||
// add the height of the toolbar itself to the bottom of the safe area so content inside SwiftUI ScrollView doesn't underflow it
|
// add the height of the toolbar itself to the bottom of the safe area so content inside SwiftUI ScrollView doesn't underflow it
|
||||||
updateAdditionalSafeAreaInsets()
|
updateAdditionalSafeAreaInsets()
|
||||||
|
@ -87,6 +94,12 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
DraftsManager.save()
|
DraftsManager.save()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.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) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
@ -116,37 +129,68 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
DraftsManager.save()
|
DraftsManager.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createToolbar() -> UIToolbar {
|
private func setupToolbarItems(toolbar: UIToolbar, input: ComposeInput?) {
|
||||||
let toolbar = UIToolbar()
|
var items: [UIBarButtonItem] = []
|
||||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
toolbar.isAccessibilityElement = true
|
items.append(UIBarButtonItem(title: "CW", style: .plain, target: self, action: #selector(cwButtonPressed)))
|
||||||
|
|
||||||
let visibilityItem = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
let visibilityItem = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
||||||
visibilityBarButtonItems.append(visibilityItem)
|
visibilityItem.tag = VISIBILITY_BAR_BUTTON_TAG
|
||||||
visibilityChanged(draft.visibility)
|
items.append(visibilityItem)
|
||||||
|
|
||||||
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))
|
|
||||||
]
|
|
||||||
|
|
||||||
if mastodonController.instanceFeatures.localOnlyPosts {
|
if mastodonController.instanceFeatures.localOnlyPosts {
|
||||||
let item = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
let item = UIBarButtonItem(image: nil, style: .plain, target: nil, action: nil)
|
||||||
toolbar.items!.insert(item, at: 2)
|
item.tag = LOCAL_ONLY_BAR_BUTTON_TAG
|
||||||
localOnlyItems.append(item)
|
items.append(item)
|
||||||
localOnlyChanged(draft.localOnly)
|
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() {
|
private func updateAdditionalSafeAreaInsets() {
|
||||||
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: toolbarHeight, right: 0)
|
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: toolbarHeight, right: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func keyboardWillShow(_ notification: Foundation.Notification) {
|
@objc private func composeKeyboardWillShow(_ notification: Foundation.Notification) {
|
||||||
keyboardWillShow(accessoryView: inputAccessoryToolbar, notification: notification)
|
keyboardWillShow(accessoryView: inputAccessoryToolbar, notification: notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +201,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
accessoryView.isHidden = false
|
accessoryView.isHidden = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func keyboardWillHide(_ notification: Foundation.Notification) {
|
@objc private func composeKeyboardWillHide(_ notification: Foundation.Notification) {
|
||||||
keyboardWillHide(accessoryView: inputAccessoryToolbar, notification: notification)
|
keyboardWillHide(accessoryView: inputAccessoryToolbar, notification: notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +233,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func keyboardDidHide(_ notification: Foundation.Notification) {
|
@objc private func composeKeyboardDidHide(_ notification: Foundation.Notification) {
|
||||||
keyboardDidHide(accessoryView: inputAccessoryToolbar, notification: notification)
|
keyboardDidHide(accessoryView: inputAccessoryToolbar, notification: notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +242,10 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func visibilityChanged(_ newVisibility: Status.Visibility) {
|
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.image = UIImage(systemName: newVisibility.imageName)
|
||||||
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
|
||||||
|
@ -212,7 +259,10 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func localOnlyChanged(_ localOnly: Bool) {
|
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 {
|
if localOnly {
|
||||||
item.image = UIImage(named: "link.broken")
|
item.image = UIImage(named: "link.broken")
|
||||||
item.accessibilityLabel = "Local-only"
|
item.accessibilityLabel = "Local-only"
|
||||||
|
@ -260,6 +310,19 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
draft.contentWarningEnabled = !draft.contentWarningEnabled
|
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() {
|
@objc func draftsButtonPresed() {
|
||||||
let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!, exclude: draft)
|
let draftsVC = DraftsTableViewController(account: mastodonController.accountInfo!, exclude: draft)
|
||||||
draftsVC.delegate = self
|
draftsVC.delegate = self
|
||||||
|
|
|
@ -31,7 +31,8 @@ class ComposeUIState: ObservableObject {
|
||||||
|
|
||||||
var composeDrawingMode: ComposeDrawingMode?
|
var composeDrawingMode: ComposeDrawingMode?
|
||||||
|
|
||||||
weak var autocompleteHandler: ComposeAutocompleteHandler?
|
var shouldEmojiAutocompletionBeginExpanded = false
|
||||||
|
@Published var currentInput: ComposeInput?
|
||||||
|
|
||||||
init(draft: Draft) {
|
init(draft: Draft) {
|
||||||
self.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 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) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
let itemWidth = NSCollectionLayoutDimension.fractionalWidth(1.0 / 10)
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let itemSize = NSCollectionLayoutSize(widthDimension: itemWidth, heightDimension: itemWidth)
|
let hSizeClass = environment.traitCollection.horizontalSizeClass
|
||||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
|
||||||
|
let itemWidth = NSCollectionLayoutDimension.fractionalWidth(1.0 / (hSizeClass == .compact ? 10 : 20))
|
||||||
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemWidth)
|
let itemSize = NSCollectionLayoutSize(widthDimension: itemWidth, heightDimension: itemWidth)
|
||||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||||
group.interItemSpacing = .fixed(4)
|
|
||||||
|
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemWidth)
|
||||||
let section = NSCollectionLayoutSection(group: group)
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||||
section.interGroupSpacing = 4
|
group.interItemSpacing = .fixed(4)
|
||||||
|
|
||||||
let layout = UICollectionViewCompositionalLayout(section: section)
|
let section = NSCollectionLayoutSection(group: group)
|
||||||
|
section.interGroupSpacing = 4
|
||||||
|
return section
|
||||||
|
}
|
||||||
|
|
||||||
super.init(collectionViewLayout: layout)
|
super.init(collectionViewLayout: layout)
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ struct EmojiPickerWrapper: UIViewControllerRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectedEmoji(_ emoji: Emoji) {
|
func selectedEmoji(_ emoji: Emoji) {
|
||||||
uiState.autocompleteHandler?.autocomplete(with: ":\(emoji.shortcode):")
|
uiState.currentInput?.autocomplete(with: ":\(emoji.shortcode):")
|
||||||
uiState.autocompleteState = nil
|
uiState.autocompleteState = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,9 +57,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
|
|
||||||
@EnvironmentObject var uiState: ComposeUIState
|
@EnvironmentObject var uiState: ComposeUIState
|
||||||
@EnvironmentObject var mastodonController: MastodonController
|
@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 {
|
func makeUIView(context: Context) -> UITextView {
|
||||||
let textView = WrappedTextView()
|
let textView = WrappedTextView()
|
||||||
|
@ -69,101 +66,9 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
textView.font = .systemFont(ofSize: 20)
|
textView.font = .systemFont(ofSize: 20)
|
||||||
textView.textContainer.lineBreakMode = .byWordWrapping
|
textView.textContainer.lineBreakMode = .byWordWrapping
|
||||||
context.coordinator.textView = textView
|
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
|
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) {
|
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||||
if context.coordinator.skipSettingTextOnNextUpdate {
|
if context.coordinator.skipSettingTextOnNextUpdate {
|
||||||
context.coordinator.skipSettingTextOnNextUpdate = false
|
context.coordinator.skipSettingTextOnNextUpdate = false
|
||||||
|
@ -171,18 +76,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
uiView.text = text
|
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.text = $text
|
||||||
context.coordinator.didChange = textDidChange
|
context.coordinator.didChange = textDidChange
|
||||||
context.coordinator.uiState = uiState
|
context.coordinator.uiState = uiState
|
||||||
|
@ -232,17 +125,23 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
command.attributes.remove(.disabled)
|
command.attributes.remove(.disabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Coordinator: NSObject, UITextViewDelegate, ComposeAutocompleteHandler, ComposeTextViewCaretScrolling {
|
class Coordinator: NSObject, UITextViewDelegate, ComposeInput, ComposeTextViewCaretScrolling {
|
||||||
weak var textView: UITextView?
|
weak var textView: UITextView?
|
||||||
var text: Binding<String>
|
var text: Binding<String>
|
||||||
var didChange: (UITextView) -> Void
|
var didChange: (UITextView) -> Void
|
||||||
var uiState: ComposeUIState
|
// break retained cycle through ComposeUIState.currentInput
|
||||||
|
unowned var uiState: ComposeUIState
|
||||||
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
var skipSettingTextOnNextUpdate = false
|
var skipSettingTextOnNextUpdate = false
|
||||||
|
|
||||||
|
var toolbarElements: [ComposeUIState.ToolbarElement] {
|
||||||
|
[.emojiPicker, .formattingButtons]
|
||||||
|
}
|
||||||
|
|
||||||
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
||||||
self.text = text
|
self.text = text
|
||||||
self.didChange = didChange
|
self.didChange = didChange
|
||||||
|
@ -256,11 +155,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
ensureCursorVisible(textView: textView)
|
ensureCursorVisible(textView: textView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func formatButtonPressed(_ sender: UIBarButtonItem) {
|
|
||||||
let format = StatusFormat.allCases[sender.tag]
|
|
||||||
applyFormat(format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyFormat(_ format: StatusFormat) {
|
func applyFormat(_ format: StatusFormat) {
|
||||||
guard let textView = textView,
|
guard let textView = textView,
|
||||||
textView.isFirstResponder,
|
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) {
|
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||||
uiState.autocompleteHandler = self
|
uiState.currentInput = self
|
||||||
updateAutocompleteState()
|
updateAutocompleteState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func textViewDidEndEditing(_ textView: UITextView) {
|
func textViewDidEndEditing(_ textView: UITextView) {
|
||||||
|
uiState.currentInput = nil
|
||||||
updateAutocompleteState()
|
updateAutocompleteState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,6 +193,10 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
self.updateAutocompleteState()
|
self.updateAutocompleteState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func beginAutocompletingEmoji() {
|
||||||
|
textView?.insertText(":")
|
||||||
|
}
|
||||||
|
|
||||||
func autocomplete(with string: String) {
|
func autocomplete(with string: String) {
|
||||||
guard let textView = textView,
|
guard let textView = textView,
|
||||||
let text = textView.text,
|
let text = textView.text,
|
||||||
|
|
|
@ -30,7 +30,7 @@ class ListTimelineViewController: TimelineTableViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editButtonPressed))
|
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editListButtonPressed))
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
@ -43,18 +43,18 @@ class ListTimelineViewController: TimelineTableViewController {
|
||||||
|
|
||||||
func presentEdit(animated: Bool) {
|
func presentEdit(animated: Bool) {
|
||||||
let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController)
|
let editListAccountsController = EditListAccountsViewController(list: list, mastodonController: mastodonController)
|
||||||
editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonPressed))
|
editListAccountsController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(editListDoneButtonPressed))
|
||||||
let navController = UINavigationController(rootViewController: editListAccountsController)
|
let navController = UINavigationController(rootViewController: editListAccountsController)
|
||||||
present(navController, animated: animated)
|
present(navController, animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@objc func editButtonPressed() {
|
@objc func editListButtonPressed() {
|
||||||
presentEdit(animated: true)
|
presentEdit(animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func doneButtonPressed() {
|
@objc func editListDoneButtonPressed() {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
|
|
||||||
// todo: show loading indicator
|
// todo: show loading indicator
|
||||||
|
|
|
@ -46,8 +46,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
preferredDisplayMode = .oneBesideSecondary
|
preferredDisplayMode = .oneBesideSecondary
|
||||||
preferredSplitBehavior = .tile
|
preferredSplitBehavior = .tile
|
||||||
presentsWithGesture = false
|
|
||||||
showsSecondaryOnlyButton = false
|
|
||||||
delegate = self
|
delegate = self
|
||||||
|
|
||||||
sidebar = MainSidebarViewController(mastodonController: mastodonController)
|
sidebar = MainSidebarViewController(mastodonController: mastodonController)
|
||||||
|
|
|
@ -56,6 +56,9 @@ struct AppearancePrefsView : View {
|
||||||
Toggle(isOn: $preferences.alwaysShowStatusVisibilityIcon) {
|
Toggle(isOn: $preferences.alwaysShowStatusVisibilityIcon) {
|
||||||
Text("Always Show Status Visibility Icons")
|
Text("Always Show Status Visibility Icons")
|
||||||
}
|
}
|
||||||
|
Toggle(isOn: $preferences.hideActionsInTimeline) {
|
||||||
|
Text("Hide Actions on Timeline")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,10 +109,20 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
case "p":
|
case "p":
|
||||||
attributed.append(NSAttributedString(string: "\n\n"))
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
case "em", "i":
|
case "em", "i":
|
||||||
let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font!
|
let currentFont: UIFont
|
||||||
|
if attributed.length == 0 {
|
||||||
|
currentFont = defaultFont
|
||||||
|
} else {
|
||||||
|
currentFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? defaultFont
|
||||||
|
}
|
||||||
attributed.addAttribute(.font, value: currentFont.withTraits(.traitItalic)!, range: attributed.fullRange)
|
attributed.addAttribute(.font, value: currentFont.withTraits(.traitItalic)!, range: attributed.fullRange)
|
||||||
case "strong", "b":
|
case "strong", "b":
|
||||||
let currentFont: UIFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? self.font!
|
let currentFont: UIFont
|
||||||
|
if attributed.length == 0 {
|
||||||
|
currentFont = defaultFont
|
||||||
|
} else {
|
||||||
|
currentFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? defaultFont
|
||||||
|
}
|
||||||
attributed.addAttribute(.font, value: currentFont.withTraits(.traitBold)!, range: attributed.fullRange)
|
attributed.addAttribute(.font, value: currentFont.withTraits(.traitBold)!, range: attributed.fullRange)
|
||||||
case "del":
|
case "del":
|
||||||
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
||||||
|
|
|
@ -22,6 +22,10 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
@IBOutlet weak var reblogLabel: EmojiLabel!
|
@IBOutlet weak var reblogLabel: EmojiLabel!
|
||||||
@IBOutlet weak var timestampLabel: UILabel!
|
@IBOutlet weak var timestampLabel: UILabel!
|
||||||
@IBOutlet weak var pinImageView: UIImageView!
|
@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 reblogStatusID: String?
|
||||||
var rebloggerID: String?
|
var rebloggerID: String?
|
||||||
|
@ -51,6 +55,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
contentTextView.defaultFont = .systemFont(ofSize: 16)
|
contentTextView.defaultFont = .systemFont(ofSize: 16)
|
||||||
|
|
||||||
avatarImageView.addInteraction(UIContextMenuInteraction(delegate: self))
|
avatarImageView.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
|
|
||||||
|
updateActionsVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func createObserversIfNecessary() {
|
override func createObserversIfNecessary() {
|
||||||
|
@ -121,9 +127,33 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
} else {
|
} else {
|
||||||
metaIndicatorsView.allowedIndicators = .all.subtracting(.reply)
|
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)
|
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
|
||||||
|
// sometimes this constraint is nil for reasons i can't discern
|
||||||
|
// not re-activating in that case doesn't seem to make a difference
|
||||||
|
actionsContainerHeightConstraint?.isActive = true
|
||||||
|
verticalStackToSuperviewConstraint.isActive = false
|
||||||
|
verticalStackToActionsContainerConstraint.isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func updateTimestamp() {
|
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
|
// 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
|
// so we bail out immediately, since there's nothing to update
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<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="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.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 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 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 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 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="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="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"/>
|
<constraint firstItem="QMP-j2-HLn" firstAttribute="leading" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="zeW-tQ-uJl"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
|
<variation key="default">
|
||||||
|
<mask key="constraints">
|
||||||
|
<exclude reference="kq7-bk-S8j"/>
|
||||||
|
</mask>
|
||||||
|
</variation>
|
||||||
</view>
|
</view>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
@ -240,6 +246,8 @@
|
||||||
</constraints>
|
</constraints>
|
||||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||||
<connections>
|
<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="attachmentsView" destination="nbq-yr-2mA" id="SVm-zl-mPb"/>
|
||||||
<outlet property="avatarImageView" destination="QMP-j2-HLn" id="xfS-v8-Gzu"/>
|
<outlet property="avatarImageView" destination="QMP-j2-HLn" id="xfS-v8-Gzu"/>
|
||||||
<outlet property="cardView" destination="LKo-VB-XWl" id="6X5-8P-Ata"/>
|
<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="replyButton" destination="rKF-yF-KIa" id="rka-q1-o4a"/>
|
||||||
<outlet property="timestampLabel" destination="35d-EA-ReR" id="Ny2-nV-nqP"/>
|
<outlet property="timestampLabel" destination="35d-EA-ReR" id="Ny2-nV-nqP"/>
|
||||||
<outlet property="usernameLabel" destination="j89-zc-SFa" id="bXX-FZ-fCp"/>
|
<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>
|
</connections>
|
||||||
<point key="canvasLocation" x="29.600000000000001" y="79.160419790104953"/>
|
<point key="canvasLocation" x="29.600000000000001" y="79.160419790104953"/>
|
||||||
</view>
|
</view>
|
||||||
|
|
Loading…
Reference in New Issue