Compare commits
6 Commits
1c871a12a1
...
16b02edf87
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 16b02edf87 | |
Shadowfacts | b8f169d0cd | |
Shadowfacts | 62a9535394 | |
Shadowfacts | 8c4ef3caa6 | |
Shadowfacts | e763d48bf3 | |
Shadowfacts | f841854c5f |
|
@ -1,5 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
## 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)
|
||||
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.
|
||||
|
||||
|
|
|
@ -2265,7 +2265,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
|
@ -2294,7 +2294,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
|
|
|
@ -58,6 +58,10 @@ struct ComposeContentWarningTextField: UIViewRepresentable {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -143,6 +147,10 @@ struct ComposeContentWarningTextField: UIViewRepresentable {
|
|||
let selectedRangeStartUTF16 = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
|
||||
let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
|
||||
|
||||
guard cursorIndex != text.startIndex else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var lastWordStartIndex = text.index(before: cursorIndex)
|
||||
while true {
|
||||
let c = text[lastWordStartIndex]
|
||||
|
|
|
@ -41,11 +41,10 @@ struct ComposeReplyView: View {
|
|||
ComposeReplyContentView(status: status) { (newHeight) in
|
||||
self.contentHeight = newHeight
|
||||
}
|
||||
.frame(height: contentHeight)
|
||||
.offset(x: -4, y: -8)
|
||||
.padding(.bottom, -8)
|
||||
}
|
||||
.frame(minHeight: 50 + 8)
|
||||
.frame(height: max(50, contentHeight ?? 0) + 8)
|
||||
}
|
||||
.padding(.bottom, -8)
|
||||
}
|
||||
|
|
|
@ -144,6 +144,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
|
||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||
uiView.text = text
|
||||
|
||||
if let visibilityButton = visibilityButton {
|
||||
visibilityButton.image = UIImage(systemName: visibility.imageName)
|
||||
updateVisibilityMenu(visibilityButton)
|
||||
|
@ -175,6 +176,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
var text: Binding<String>
|
||||
var didChange: (UITextView) -> Void
|
||||
var uiState: ComposeUIState
|
||||
private var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||
|
||||
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
||||
self.text = text
|
||||
|
@ -185,6 +187,54 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
func textViewDidChange(_ textView: UITextView) {
|
||||
text.wrappedValue = textView.text
|
||||
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) {
|
||||
|
@ -227,6 +277,17 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
|
||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
switch item {
|
||||
case let .selected(instance):
|
||||
case let .selected(_, instance):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell
|
||||
cell.updateUI(instance: instance)
|
||||
return cell
|
||||
|
@ -113,8 +113,9 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
|
||||
private func updateSpecificInstance(domain: String) {
|
||||
let components = parseURLComponents(input: domain)
|
||||
let url = components.url!
|
||||
|
||||
let client = Client(baseURL: components.url!)
|
||||
let client = Client(baseURL: url)
|
||||
let request = Client.getInstance()
|
||||
client.run(request) { (response) in
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
|
@ -126,7 +127,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
if !snapshot.sectionIdentifiers.contains(.selected) {
|
||||
snapshot.appendSections([.selected])
|
||||
}
|
||||
snapshot.appendItems([.selected(instance)], toSection: .selected)
|
||||
snapshot.appendItems([.selected(url, instance)], toSection: .selected)
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(snapshot)
|
||||
}
|
||||
|
@ -168,10 +169,11 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
return
|
||||
}
|
||||
switch item {
|
||||
case let .selected(instance):
|
||||
// we can't just turn the URI string from the API into a URL instance, because Mastodon only includes the domain in the "URI"
|
||||
let components = parseURLComponents(input: instance.uri)
|
||||
delegate.didSelectInstance(url: components.url!)
|
||||
case let .selected(url, _):
|
||||
// We can't rely on the URI reported by the instance API endpoint, because improperly configured instances may
|
||||
// return a domain that they don't listen on. Instead, use the actual base URL that was used to make the /api/v1/instance
|
||||
// request, since we know for certain that succeeded, otherwise this item wouldn't exist.
|
||||
delegate.didSelectInstance(url: url)
|
||||
case let .recommended(instance):
|
||||
var components = URLComponents()
|
||||
components.scheme = "https"
|
||||
|
@ -188,13 +190,13 @@ extension InstanceSelectorTableViewController {
|
|||
case recommendedInstances
|
||||
}
|
||||
enum Item: Equatable, Hashable {
|
||||
case selected(Instance)
|
||||
case selected(URL, Instance)
|
||||
case recommended(InstanceSelector.Instance)
|
||||
|
||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
if case let .selected(instance) = lhs,
|
||||
case let .selected(other) = rhs {
|
||||
return instance.uri == other.uri
|
||||
if case let .selected(url, instance) = lhs,
|
||||
case let .selected(otherUrl, other) = rhs {
|
||||
return url == otherUrl && instance.uri == other.uri
|
||||
} else if case let .recommended(instance) = lhs,
|
||||
case let .recommended(other) = rhs {
|
||||
return instance.domain == other.domain
|
||||
|
@ -204,8 +206,9 @@ extension InstanceSelectorTableViewController {
|
|||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case let .selected(instance):
|
||||
case let .selected(url, instance):
|
||||
hasher.combine(Section.selected)
|
||||
hasher.combine(url)
|
||||
hasher.combine(instance.uri)
|
||||
case let .recommended(instance):
|
||||
hasher.combine(Section.recommendedInstances)
|
||||
|
|
Loading…
Reference in New Issue