Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions Android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,33 @@ configurations.configureEach {
}

val repoRoot = rootProject.projectDir.parentFile
// Supported ABIs: arm64-v8a for devices, x86_64 for the Android emulator VM.
val androidNativeAbis = (System.getenv("LOGSEQ_CHAT_ANDROID_ABIS")
?: System.getenv("LOGSEQ_CHAT_ANDROID_ABI")
?: "arm64-v8a")
.split(Regex("[,\\s]+"))
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()

tasks.register<Exec>("buildAndroidNativeCore") {
workingDir = repoRoot
commandLine("bash", "scripts/build-android-native.sh")
val androidNativeCoreTasks = androidNativeAbis.map { abi ->
tasks.register<Exec>("buildAndroidNativeCore_${abi.replace('-', '_')}") {
workingDir = repoRoot
environment("LOGSEQ_CHAT_ANDROID_ABI", abi)
listOf(
"ANDROID_HOME",
"ANDROID_SDK_ROOT",
"ANDROID_NDK_HOME",
"LOGSEQ_CHAT_BUILD_JOBS"
).forEach { key ->
System.getenv(key)?.let { value -> environment(key, value) }
}
commandLine("bash", "scripts/build-android-native.sh")
}
}

tasks.register("buildAndroidNativeCore") {
dependsOn(androidNativeCoreTasks)
}

tasks.named("preBuild") {
Expand Down
2 changes: 1 addition & 1 deletion Android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
16 changes: 14 additions & 2 deletions Android/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,20 @@ pluginManagement {
//System.setProperty("BUILT_PRODUCTS_DIR", "${System.getProperty("user.home")}/Library/Developer/Xcode/DerivedData/MySkipProject-HASH/Build/Products/Debug-iphonesimulator")

val skipPluginResult = providers.exec {
commandLine("/bin/sh", "-c", "skip plugin --prebuild --package-path '${settings.rootDir.parent}' --plugin-ref '${pluginPath.absolutePath}'")
environment("PATH", "${System.getenv("PATH")}:/opt/homebrew/bin")
// Xcode `xcrun` is required for Skip's iOS prebuild. On Linux emulator
// hosts, fall back to `--no-prebuild` and consume existing skipstone output.
commandLine(
"/bin/sh",
"-c",
"""
if command -v xcrun >/dev/null 2>&1; then
skip plugin --prebuild --package-path '${settings.rootDir.parent}' --plugin-ref '${pluginPath.absolutePath}'
else
skip plugin --no-prebuild --package-path '${settings.rootDir.parent}' --plugin-ref '${pluginPath.absolutePath}'
fi
""".trimIndent()
)
environment("PATH", "${System.getenv("PATH")}:/opt/homebrew/bin:${System.getProperty("user.home")}/opt/skip/skip.artifactbundle/bin")
}
val skipPluginOutput = skipPluginResult.standardOutput.asText.get()
print(skipPluginOutput)
Expand Down
22 changes: 12 additions & 10 deletions Sources/LogseqChat/LogseqChatApp.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Foundation
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
import OSLog
#endif
import SwiftUI
import LogseqChatModel

Expand All @@ -9,34 +11,34 @@ import UIKit
#endif

struct LogseqAppLogger {
#if !SKIP
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
private let systemLogger = os.Logger(subsystem: "com.logseq.chat", category: "LogseqChat")
#endif

func debug(_ message: String) {
LogseqRuntimeLog.shared.append(level: .debug, source: .ui, message: message)
#if SKIP
print(message)
#else
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
systemLogger.debug("\(message, privacy: .public)")
#else
print(message)
#endif
}

func info(_ message: String) {
LogseqRuntimeLog.shared.append(level: .info, source: .ui, message: message)
#if SKIP
print(message)
#else
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
systemLogger.info("\(message, privacy: .public)")
#else
print(message)
#endif
}

func error(_ message: String) {
LogseqRuntimeLog.shared.append(level: .error, source: .ui, message: message)
#if SKIP
print(message)
#else
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
systemLogger.error("\(message, privacy: .public)")
#else
print(message)
#endif
}
}
Expand Down
36 changes: 30 additions & 6 deletions Sources/LogseqChatModel/Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public enum LogseqAuthenticationState: String, Sendable {
case signingOut
}

public enum LogseqAuthenticationError: Error, LocalizedError {
public enum LogseqAuthenticationError: Error, LocalizedError, Equatable {
case notSignedIn

public var errorDescription: String? {
Expand Down Expand Up @@ -52,9 +52,12 @@ public enum LogseqAuthenticationError: Error, LocalizedError {
}
state = .signedIn
onAccessToken(token)
} catch is LogseqAuthenticationError {
state = .signedOut
onAccessToken(nil)
} catch {
state = .signedOut
errorMessage = error.localizedDescription
errorMessage = Self.displayedMessage(for: error)
onAccessToken(nil)
}
}
Expand All @@ -71,7 +74,7 @@ public enum LogseqAuthenticationError: Error, LocalizedError {
onAccessToken(token)
} catch {
state = .signedOut
errorMessage = error.localizedDescription
errorMessage = Self.displayedMessage(for: error)
onAccessToken(nil)
}
}
Expand All @@ -82,7 +85,7 @@ public enum LogseqAuthenticationError: Error, LocalizedError {
do {
try await provider.signOut()
} catch {
errorMessage = error.localizedDescription
errorMessage = Self.displayedMessage(for: error)
}
state = .signedOut
onAccessToken(nil)
Expand All @@ -92,19 +95,40 @@ public enum LogseqAuthenticationError: Error, LocalizedError {
do {
guard let token = try await provider.accessToken(), !token.isEmpty else {
state = .signedOut
errorMessage = LogseqAuthenticationError.notSignedIn.localizedDescription
errorMessage = nil
onAccessToken(nil)
throw LogseqAuthenticationError.notSignedIn
}
state = .signedIn
errorMessage = nil
onAccessToken(token)
return token
} catch is LogseqAuthenticationError {
state = .signedOut
errorMessage = nil
onAccessToken(nil)
throw LogseqAuthenticationError.notSignedIn
} catch {
state = .signedOut
errorMessage = error.localizedDescription
errorMessage = Self.displayedMessage(for: error)
onAccessToken(nil)
throw error
}
}

private static func displayedMessage(for error: Error) -> String {
if let authenticationError = error as? LogseqAuthenticationError {
return authenticationError.errorDescription ?? "Sign in to connect to Logseq Sync."
}
if let localized = error as? LocalizedError,
let description = localized.errorDescription,
!description.isEmpty {
return description
}
let description = error.localizedDescription
if description.contains("$") || description.contains("Case") {
return "Something went wrong. Try signing in again."
}
return description
Comment on lines +128 to +132
}
}
10 changes: 10 additions & 0 deletions Sources/LogseqChatModel/GraphSyncHTTP.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Foundation
#if !SKIP
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import LogseqChatCoreABI
#endif

Expand Down Expand Up @@ -184,7 +187,14 @@ public enum LogseqGraphSyncHTTP {
}
var downloadRequest = URLRequest(url: downloadURL.absoluteURL)
downloadRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
#if os(iOS) || os(macOS)
let (temporaryURL, downloadResponse) = try await URLSession.shared.download(for: downloadRequest)
#else
let (downloadData, downloadResponse) = try await URLSession.shared.data(for: downloadRequest)
let temporaryURL = FileManager.default.temporaryDirectory
.appendingPathComponent("logseq-graph-\(UUID().uuidString).partial")
try downloadData.write(to: temporaryURL, options: .atomic)
#endif
try requireSuccess(downloadResponse)
guard let http = downloadResponse as? HTTPURLResponse,
let rowCountText = http.value(forHTTPHeaderField: "x-snapshot-row-count"),
Expand Down
3 changes: 3 additions & 0 deletions Sources/LogseqChatModel/PendingSyncTransport.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Foundation

#if !SKIP
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public enum LogseqPendingSyncDuplicateAssetResolver {
public static func isDuplicate(status: Int, body: String) -> Bool {
guard status == 409,
Expand Down
29 changes: 20 additions & 9 deletions Sources/LogseqChatModel/ViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import Foundation
import Observation
#if !os(Android)
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
import OSLog
#endif
#if !SKIP
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
#endif
#if SKIP
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand All @@ -13,25 +18,25 @@ import LogseqChatCoreABI
#endif

private struct LogseqModelLogger {
#if !os(Android)
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
private let logger = Logger(subsystem: "logseq.chat.model", category: "LogseqChatModel")
#endif

func info(_ message: String) {
LogseqRuntimeLog.shared.append(level: .info, source: .core, message: message)
#if os(Android)
print(message)
#else
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
logger.info("\(message, privacy: .public)")
#else
print(message)
#endif
}

func error(_ message: String) {
LogseqRuntimeLog.shared.append(level: .error, source: .core, message: message)
#if os(Android)
print(message)
#else
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
logger.error("\(message, privacy: .public)")
#else
print(message)
#endif
}
}
Expand Down Expand Up @@ -833,7 +838,7 @@ private struct DeletePagePayload: Encodable {
syncError = LogseqChatCoreError(code: "sse_connection_failed", message: "\(error)")
}
return false
#else
#elseif os(iOS) || os(macOS)
do {
let request = try LogseqGraphSyncHTTP.eventsRequest(
baseURL: baseURL,
Expand Down Expand Up @@ -897,6 +902,12 @@ private struct DeletePagePayload: Encodable {
}
}
return false
#else
lastError = LogseqChatCoreError(
code: "sse_unsupported_platform",
message: "Graph event streaming requires iOS, macOS, or Android"
)
return false
#endif
}

Expand Down
18 changes: 18 additions & 0 deletions Tests/LogseqChatModelTests/AuthenticationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ private struct FailingCognitoProvider: LogseqCognitoProviding {
#expect(await provider.recordedSignOutCount() == 1)
}

@Test @MainActor func missingAccessTokenDoesNotSurfaceARawErrorOnTheSignInScreen() async {
let auth = LogseqAuthenticationStore(provider: FakeCognitoProvider(token: nil))

await auth.restore()
#expect(auth.state == .signedOut)
#expect(auth.errorMessage == nil)

do {
_ = try await auth.accessToken()
#expect(1 == 0, "Expected a missing access token to throw")
} catch is LogseqAuthenticationError {
#expect(auth.state == .signedOut)
#expect(auth.errorMessage == nil)
} catch {
#expect(1 == 0, "Expected LogseqAuthenticationError.notSignedIn")
}
Comment on lines +113 to +121
}

@Test @MainActor func accessTokenFailureUpdatesObservableAuthenticationState() async {
var published: [String?] = []
let auth = LogseqAuthenticationStore(provider: FailingCognitoProvider()) { token in
Expand Down
8 changes: 6 additions & 2 deletions scripts/test-android-native-script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ require_text() {
fail "missing $path"
return
fi
grep -F "$pattern" "$repo_root/$path" >/dev/null || fail "$path does not contain: $pattern"
grep -F -- "$pattern" "$repo_root/$path" >/dev/null || fail "$path does not contain: $pattern"
}

reject_text() {
local path=$1
local pattern=$2
if [[ -f $repo_root/$path ]] && grep -F "$pattern" "$repo_root/$path" >/dev/null; then
if [[ -f $repo_root/$path ]] && grep -F -- "$pattern" "$repo_root/$path" >/dev/null; then
fail "$path unexpectedly contains: $pattern"
fi
}
Expand Down Expand Up @@ -77,6 +77,10 @@ require_text "Sources/LogseqChat/Skip/AndroidAssetImporter.kt" "contentResolver.
reject_text "scripts/build-android-native.sh" "logseq_chat_https_stub.c"
require_text "Android/app/build.gradle.kts" "buildAndroidNativeCore"
require_text "Android/app/build.gradle.kts" "scripts/build-android-native.sh"
require_text "Android/app/build.gradle.kts" "LOGSEQ_CHAT_ANDROID_ABIS"
require_text "Android/app/build.gradle.kts" "x86_64"
require_text "scripts/build-android-native.sh" "x86_64"
require_text "Android/gradle/wrapper/gradle-wrapper.properties" "gradle-9.4.1-bin.zip"

if [[ $failures -ne 0 ]]; then
exit 1
Expand Down