Compare commits

..

No commits in common. "5d9f4b8ea8179808ae443acb27f6089dac7f657f" and "1c871a12a118dfd4675cbc3524c8ea88682aafdf" have entirely different histories.

6 changed files with 16 additions and 101 deletions

View File

@ -1,19 +1,5 @@
# Changelog # Changelog
## 2020.1 (13)
This is another quick build to fix a couple of severe issues on the Compose screen.
Features/Improvements:
- When composing posts, ensure the cursor is always visible and does not scroll below the keyboard while typing
Bugfixes:
- Fix builtin iOS keyboard suggestions not working in text fields on the Compose screen
- Fix crash when ending dictation in the CW field
- Fix broken layout on the Compose when replying to certain posts
## 2020.1 (12)
This build is a hotfix for the issue of being unable to login to certain instances. The changelog for the previous build is included below.
## 2020.1 (11) ## 2020.1 (11)
This release is primarily focused on bug fixes with the one key feature of autocomplete suggestions when typing in the Compose screen. It also fixes an issue on the various new sizes of iPhone 12, so if you're getting a new device, make sure to update. This release is primarily focused on bug fixes with the one key feature of autocomplete suggestions when typing in the Compose screen. It also fixes an issue on the various new sizes of iPhone 12, so if you're getting a new device, make sure to update.

View File

@ -2265,7 +2265,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 = 13; CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
@ -2294,7 +2294,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 = 13; CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = V4WK9KR9U2; DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist; INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;

View File

@ -58,10 +58,6 @@ struct ComposeContentWarningTextField: UIViewRepresentable {
} }
func textFieldDidChangeSelection(_ textField: UITextField) { func textFieldDidChangeSelection(_ textField: UITextField) {
// Update text binding before potentially triggering SwiftUI view update.
// See comment in MainComposeTextView.Coordinator.textViewDidChangeSelection
text.wrappedValue = textField.text ?? ""
updateAutocompleteState(textField: textField) updateAutocompleteState(textField: textField)
} }
@ -147,10 +143,6 @@ struct ComposeContentWarningTextField: UIViewRepresentable {
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start) let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16) let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
guard cursorIndex != text.startIndex else {
return nil
}
var lastWordStartIndex = text.index(before: cursorIndex) var lastWordStartIndex = text.index(before: cursorIndex)
while true { while true {
let c = text[lastWordStartIndex] let c = text[lastWordStartIndex]

View File

@ -41,10 +41,11 @@ struct ComposeReplyView: View {
ComposeReplyContentView(status: status) { (newHeight) in ComposeReplyContentView(status: status) { (newHeight) in
self.contentHeight = newHeight self.contentHeight = newHeight
} }
.frame(height: contentHeight)
.offset(x: -4, y: -8) .offset(x: -4, y: -8)
.padding(.bottom, -8) .padding(.bottom, -8)
} }
.frame(height: max(50, contentHeight ?? 0) + 8) .frame(minHeight: 50 + 8)
} }
.padding(.bottom, -8) .padding(.bottom, -8)
} }

View File

@ -144,7 +144,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
func updateUIView(_ uiView: UITextView, context: Context) { func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text uiView.text = text
if let visibilityButton = visibilityButton { if let visibilityButton = visibilityButton {
visibilityButton.image = UIImage(systemName: visibility.imageName) visibilityButton.image = UIImage(systemName: visibility.imageName)
updateVisibilityMenu(visibilityButton) updateVisibilityMenu(visibilityButton)
@ -176,7 +175,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
var text: Binding<String> var text: Binding<String>
var didChange: (UITextView) -> Void var didChange: (UITextView) -> Void
var uiState: ComposeUIState var uiState: ComposeUIState
private var caretScrollPositionAnimator: UIViewPropertyAnimator?
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
@ -187,54 +185,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
func textViewDidChange(_ textView: UITextView) { func textViewDidChange(_ textView: UITextView) {
text.wrappedValue = textView.text text.wrappedValue = textView.text
didChange(textView) didChange(textView)
ensureCursorVisible()
}
private func ensureCursorVisible() {
guard let textView = textView,
textView.isFirstResponder,
let range = textView.selectedTextRange,
let scrollView = findParentScrollView() else {
return
}
// We use a UIViewProperty animator to change the scroll view position so that we can store the currently
// running one on the Coordinator. This allows us to cancel the running one, preventing multiple animations
// from attempting to change the scroll view offset simultaneously, causing it to jitter around. This can
// happen if the user is pressing return and quickly creating many new lines.
if let existing = caretScrollPositionAnimator {
existing.stopAnimation(true)
}
let cursorRect = textView.caretRect(for: range.start)
var rectToMakeVisible = textView.convert(cursorRect, to: scrollView)
// move Y position of the rect that will be made visible down by the cursor's height so that there's
// some space between the bottom of the line of text being edited and the top of the keyboard
rectToMakeVisible.origin.y += cursorRect.height
let animator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) {
scrollView.scrollRectToVisible(rectToMakeVisible, animated: false)
}
self.caretScrollPositionAnimator = animator
animator.startAnimation()
}
private func findParentScrollView() -> UIScrollView? {
guard let textView = textView else { return nil }
var current: UIView = textView
while let superview = current.superview {
if let scrollView = superview as? UIScrollView {
return scrollView
} else {
current = superview
}
}
return nil
} }
@objc func formatButtonPressed(_ sender: UIBarButtonItem) { @objc func formatButtonPressed(_ sender: UIBarButtonItem) {
@ -277,17 +227,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
} }
func textViewDidChangeSelection(_ textView: UITextView) { func textViewDidChangeSelection(_ textView: UITextView) {
// Update the value of the text binding.
// Sometimes, when the user accepts an autocomplete suggestion from the system keyboard, the system
// calls didChangeSelection before textDidChange, resulting in a loop where the updating the Tusker autocomplete
// state in didChangeSection (via updateAutocompleteState) triggers a new SwiftUI view update,
// but when that SwiftUI update is handled, the model still has the old text (from prior to accepting the autocomplete
// suggestion), meaning the UITextView's text gets set back to whatever it was prior to the system autocomplete.
// To work around that, we also update the text binding in didChangeSelection, to ensure that, if the autocomplete state
// does change and trigger a SwiftUI update, the binding will have the correct text that was produced by the system
// autocompletion.
text.wrappedValue = textView.text ?? ""
updateAutocompleteState() updateAutocompleteState()
} }

View File

@ -57,7 +57,7 @@ class InstanceSelectorTableViewController: UITableViewController {
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
switch item { switch item {
case let .selected(_, instance): case let .selected(instance):
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
cell.updateUI(instance: instance) cell.updateUI(instance: instance)
return cell return cell
@ -113,9 +113,8 @@ class InstanceSelectorTableViewController: UITableViewController {
private func updateSpecificInstance(domain: String) { private func updateSpecificInstance(domain: String) {
let components = parseURLComponents(input: domain) let components = parseURLComponents(input: domain)
let url = components.url!
let client = Client(baseURL: url) let client = Client(baseURL: components.url!)
let request = Client.getInstance() let request = Client.getInstance()
client.run(request) { (response) in client.run(request) { (response) in
var snapshot = self.dataSource.snapshot() var snapshot = self.dataSource.snapshot()
@ -127,7 +126,7 @@ class InstanceSelectorTableViewController: UITableViewController {
if !snapshot.sectionIdentifiers.contains(.selected) { if !snapshot.sectionIdentifiers.contains(.selected) {
snapshot.appendSections([.selected]) snapshot.appendSections([.selected])
} }
snapshot.appendItems([.selected(url, instance)], toSection: .selected) snapshot.appendItems([.selected(instance)], toSection: .selected)
DispatchQueue.main.async { DispatchQueue.main.async {
self.dataSource.apply(snapshot) self.dataSource.apply(snapshot)
} }
@ -169,11 +168,10 @@ class InstanceSelectorTableViewController: UITableViewController {
return return
} }
switch item { switch item {
case let .selected(url, _): case let .selected(instance):
// We can't rely on the URI reported by the instance API endpoint, because improperly configured instances may // we can't just turn the URI string from the API into a URL instance, because Mastodon only includes the domain in the "URI"
// return a domain that they don't listen on. Instead, use the actual base URL that was used to make the /api/v1/instance let components = parseURLComponents(input: instance.uri)
// request, since we know for certain that succeeded, otherwise this item wouldn't exist. delegate.didSelectInstance(url: components.url!)
delegate.didSelectInstance(url: url)
case let .recommended(instance): case let .recommended(instance):
var components = URLComponents() var components = URLComponents()
components.scheme = "https" components.scheme = "https"
@ -190,13 +188,13 @@ extension InstanceSelectorTableViewController {
case recommendedInstances case recommendedInstances
} }
enum Item: Equatable, Hashable { enum Item: Equatable, Hashable {
case selected(URL, Instance) case selected(Instance)
case recommended(InstanceSelector.Instance) case recommended(InstanceSelector.Instance)
static func ==(lhs: Item, rhs: Item) -> Bool { static func ==(lhs: Item, rhs: Item) -> Bool {
if case let .selected(url, instance) = lhs, if case let .selected(instance) = lhs,
case let .selected(otherUrl, other) = rhs { case let .selected(other) = rhs {
return url == otherUrl && instance.uri == other.uri return instance.uri == other.uri
} else if case let .recommended(instance) = lhs, } else if case let .recommended(instance) = lhs,
case let .recommended(other) = rhs { case let .recommended(other) = rhs {
return instance.domain == other.domain return instance.domain == other.domain
@ -206,9 +204,8 @@ extension InstanceSelectorTableViewController {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
switch self { switch self {
case let .selected(url, instance): case let .selected(instance):
hasher.combine(Section.selected) hasher.combine(Section.selected)
hasher.combine(url)
hasher.combine(instance.uri) hasher.combine(instance.uri)
case let .recommended(instance): case let .recommended(instance):
hasher.combine(Section.recommendedInstances) hasher.combine(Section.recommendedInstances)