Compare commits
No commits in common. "16b02edf871c547d1391ae7f923c67e6519368bf" and "1c871a12a118dfd4675cbc3524c8ea88682aafdf" have entirely different histories.
16b02edf87
...
1c871a12a1
|
@ -1,8 +1,5 @@
|
||||||
# Changelog
|
# 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)
|
## 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.
|
||||||
|
|
||||||
|
|
|
@ -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 = 12;
|
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 = 12;
|
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;
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue