Compare commits

..

3 Commits

Author SHA1 Message Date
Shadowfacts aa8819ae16 Enable expansion tooltips 2023-07-02 18:21:44 -07:00
Shadowfacts ee77ee53f4 Assorted stuff I no longer recall 2023-07-02 18:10:52 -07:00
Shadowfacts 29377a31c8 Fix app name in storyboard 2023-07-02 18:09:19 -07:00
4 changed files with 84 additions and 32 deletions

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21179.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS"> <document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="22113.3" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21179.7"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22113.3"/>
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/> <capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="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"/>
</dependencies> </dependencies>
@ -13,11 +13,11 @@
<application id="hnw-xV-0zn" sceneMemberID="viewController"> <application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6"> <menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items> <items>
<menuItem title="StoryboardTest" id="1Xt-HY-uBw"> <menuItem title="MastoSearch" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="StoryboardTest" systemMenu="apple" id="uQy-DD-JDr"> <menu key="submenu" title="MastoSearch" systemMenu="apple" id="uQy-DD-JDr">
<items> <items>
<menuItem title="About StoryboardTest" id="5kV-Vb-QxS"> <menuItem title="About MastoSearch" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<connections> <connections>
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/> <action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
@ -31,7 +31,7 @@
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/> <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/> <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide StoryboardTest" keyEquivalent="h" id="Olw-nP-bQN"> <menuItem title="Hide MastoSearch" keyEquivalent="h" id="Olw-nP-bQN">
<connections> <connections>
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/> <action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
</connections> </connections>
@ -49,7 +49,7 @@
</connections> </connections>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/> <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit StoryboardTest" keyEquivalent="q" id="4sb-4s-VLi"> <menuItem title="Quit MastoSearch" keyEquivalent="q" id="4sb-4s-VLi">
<connections> <connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/> <action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
</connections> </connections>
@ -628,7 +628,7 @@
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ"> <menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
<items> <items>
<menuItem title="StoryboardTest Help" keyEquivalent="?" id="FKE-Sm-Kum"> <menuItem title="MastoSearch Help" keyEquivalent="?" id="FKE-Sm-Kum">
<connections> <connections>
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/> <action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
</connections> </connections>
@ -665,7 +665,7 @@
<allowedToolbarItems> <allowedToolbarItems>
<searchToolbarItem implicitItemIdentifier="870CAC86-86B2-48FF-8A41-A5A04E39C9E7" label="Search" paletteLabel="Search" visibilityPriority="1001" id="Ant-Xf-lN0"> <searchToolbarItem implicitItemIdentifier="870CAC86-86B2-48FF-8A41-A5A04E39C9E7" label="Search" paletteLabel="Search" visibilityPriority="1001" id="Ant-Xf-lN0">
<nil key="toolTip"/> <nil key="toolTip"/>
<searchField key="view" verticalHuggingPriority="750" textCompletion="NO" id="gzh-NI-QNQ"> <searchField key="view" focusRingType="none" verticalHuggingPriority="750" textCompletion="NO" id="gzh-NI-QNQ">
<rect key="frame" x="0.0" y="0.0" width="100" height="21"/> <rect key="frame" x="0.0" y="0.0" width="100" height="21"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="KqM-9t-rqh"> <searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="KqM-9t-rqh">
@ -737,7 +737,7 @@
<rect key="frame" x="8" y="0.0" width="146" height="24"/> <rect key="frame" x="8" y="0.0" width="146" height="24"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<textField identifier="dateCell" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kPp-F2-rtb"> <textField identifier="dateCell" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kPp-F2-rtb">
<rect key="frame" x="0.0" y="4" width="146" height="16"/> <rect key="frame" x="0.0" y="4" width="146" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="Gz8-RC-H28"> <textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="Gz8-RC-H28">
@ -770,7 +770,7 @@
<rect key="frame" x="171" y="0.0" width="173" height="24"/> <rect key="frame" x="171" y="0.0" width="173" height="24"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<textField identifier="contentWarningCell" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Zz1-AR-4EW"> <textField identifier="contentWarningCell" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Zz1-AR-4EW">
<rect key="frame" x="0.0" y="4" width="173" height="16"/> <rect key="frame" x="0.0" y="4" width="173" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="NPl-CE-9Tj"> <textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="NPl-CE-9Tj">
@ -803,10 +803,10 @@
<rect key="frame" x="361" y="0.0" width="303" height="17"/> <rect key="frame" x="361" y="0.0" width="303" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Hwo-JH-icI"> <textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Hwo-JH-icI">
<rect key="frame" x="0.0" y="0.0" width="303" height="16"/> <rect key="frame" x="0.0" y="0.0" width="303" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Table View Cell" id="caK-oy-Tyh"> <textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="Table View Cell" id="caK-oy-Tyh">
<font key="font" usesAppearanceFont="YES"/> <font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>

View File

@ -53,7 +53,7 @@ public struct APIController {
} }
let response = response as! HTTPURLResponse let response = response as! HTTPURLResponse
guard response.statusCode == 200 else { guard response.statusCode == 200 else {
completion(.failure(.unexpectedStatusCode(response.statusCode))) completion(.failure(.unexpectedStatusCode(response.statusCode, response)))
return return
} }
guard let data = data else { guard let data = data else {
@ -126,13 +126,17 @@ extension APIController {
public enum RequestRange { public enum RequestRange {
case `default` case `default`
case after(String) case after(String)
case before(String)
var queryParameters: [URLQueryItem] { var queryParameters: [URLQueryItem] {
switch self { switch self {
case .default: case .default:
return [] return []
case .after(let id): case .after(let id):
return [URLQueryItem(name: "min_id", value: id)] // 40 is the most mastodon will return at once
return [URLQueryItem(name: "min_id", value: id), URLQueryItem(name: "count", value: "40")]
case .before(let id):
return [URLQueryItem(name: "max_id", value: id), URLQueryItem(name: "count", value: "40")]
} }
} }
} }
@ -176,14 +180,14 @@ extension APIController {
extension APIController { extension APIController {
public enum Error: Swift.Error { public enum Error: Swift.Error {
case unexpectedStatusCode(Int) case unexpectedStatusCode(Int, HTTPURLResponse)
case error(Swift.Error) case error(Swift.Error)
case noData case noData
case decoding(Swift.Error) case decoding(Swift.Error)
public var localizedDescription: String { public var localizedDescription: String {
switch self { switch self {
case .unexpectedStatusCode(let code): case .unexpectedStatusCode(let code, _):
return "Unexpected status code \(code)" return "Unexpected status code \(code)"
case .error(let inner): case .error(let inner):
return inner.localizedDescription return inner.localizedDescription

View File

@ -137,10 +137,17 @@ public class DatabaseController {
} }
} }
public func getNewestStatus(completion: @escaping (Status?) -> Void) { public func getNewestAndOldestStatuses(completion: @escaping ((Status, Status)?) -> Void) {
queue.inDatabase { db in queue.inDatabase { db in
let results = try! db.executeQuery("SELECT * FROM statuses ORDER BY published DESC LIMIT 1", values: nil) let results = try! db.executeQuery("SELECT * FROM statuses ORDER BY published DESC LIMIT 1", values: nil)
completion(StatusSequence(results: results).makeIterator().next()) if let newest = StatusSequence(results: results).makeIterator().next() {
let results2 = try! db.executeQuery("SELECT * FROM statuses ORDER BY published ASC LIMIT 1", values: nil)
// if there was a newest, there must also be an oldest
let oldest = StatusSequence(results: results2).makeIterator().next()!
completion((newest, oldest))
} else {
completion(nil)
}
} }
} }

View File

@ -19,31 +19,50 @@ public class SyncController {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Sync") private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Sync")
private var syncTotal = 0 private var syncTotal = 0
private let dateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()
public func syncStatuses(errorHandler: @escaping (APIController.Error) -> Void) { public func syncStatuses(errorHandler: @escaping (APIController.Error) -> Void) {
DatabaseController.shared.getNewestStatus { status in DatabaseController.shared.getNewestAndOldestStatuses { results in
guard let status else { if let results {
return self.logger.log("Starting sync...")
self.syncTotal = 0
self.syncStatuses(direction: .newer, range: .after(results.0.id), errorHandler: errorHandler)
self.syncStatuses(direction: .older, range: .before(results.1.id), errorHandler: errorHandler)
} else {
self.logger.log("No newest, starting backwards sync...")
self.syncTotal = 0
self.syncStatuses(direction: .older, range: .default, errorHandler: errorHandler)
} }
self.logger.log("Starting sync...")
self.syncTotal = 0
self.syncStatuses(range: .after(status.id), errorHandler: errorHandler)
} }
} }
private func syncStatuses(range: APIController.RequestRange, errorHandler: @escaping (APIController.Error) -> Void) { private func syncStatuses(direction: Direction, range: APIController.RequestRange, errorHandler: @escaping (APIController.Error) -> Void) {
APIController.shared.getStatuses(range: range) { response in APIController.shared.getStatuses(range: range) { response in
switch response { switch response {
case .failure(let error): case .failure(let error):
self.logger.error("Erorr syncing statuses: \(String(describing: error), privacy: .public)") self.logger.error("Error syncing statuses: \(String(describing: error), privacy: .public)")
DispatchQueue.main.async { if case .unexpectedStatusCode(_, let resp) = error,
errorHandler(error) resp.value(forHTTPHeaderField: "x-ratelimit-remaining") == "0",
let reset = resp.value(forHTTPHeaderField: "x-ratelimit-reset"),
let date = self.dateFormatter.date(from: reset) {
self.logger.info("Rate limited, continuing at \(date)")
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(Int(ceil(date.timeIntervalSinceNow)))) {
self.syncStatuses(direction: direction, range: range, errorHandler: errorHandler)
}
} else {
DispatchQueue.main.async {
errorHandler(error)
}
} }
case .success(let statuses): case .success(let statuses):
guard statuses.count > 0 else { guard statuses.count > 0 else {
DispatchQueue.main.async { DispatchQueue.main.async {
self.logger.log("Finished sync of \(self.syncTotal, privacy: .public) statuses") self.logger.log("Finished sync of \(self.syncTotal, privacy: .public) \(direction.name) statuses")
self.onSync.send() self.onSync.send()
} }
return return
@ -62,7 +81,29 @@ public class SyncController {
self.syncTotal += statuses.count self.syncTotal += statuses.count
self.syncStatuses(range: .after(statuses.first!.id), errorHandler: errorHandler) self.syncStatuses(direction: direction, range: direction.nextRange(statuses: statuses), errorHandler: errorHandler)
}
}
}
enum Direction {
case newer, older
var name: String {
switch self {
case .newer:
return "newer"
case .older:
return "older"
}
}
func nextRange(statuses: [APIController.Status]) -> APIController.RequestRange {
switch self {
case .newer:
return .after(statuses.first!.id)
case .older:
return .before(statuses.last!.id)
} }
} }
} }