Skip to content
Merged
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
15 changes: 15 additions & 0 deletions Plugins/Official/LinuxDoSupport/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key><string>en</string>
<key>CFBundleExecutable</key><string>LitheLinuxDoSupportPlugin</string>
<key>CFBundleIdentifier</key><string>dev.lithe.plugin.linux-do-support.bundle</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleName</key><string>LINUX DO Support</string>
<key>CFBundlePackageType</key><string>BNDL</string>
<key>CFBundleShortVersionString</key><string>0.3.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>NSPrincipalClass</key><string>LitheLinuxDoSupportPluginEntrypoint</string>
</dict>
</plist>
49 changes: 49 additions & 0 deletions Plugins/Official/LinuxDoSupport/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"schemaVersion": 1,
"id": "dev.lithe.plugin.linux-do-support",
"displayName": "LINUX DO Support",
"version": "0.3.0",
"apiVersion": 1,
"hostCompatibility": {
"minimum": "0.3.0",
"maximumExclusive": "0.4.0"
},
"vendor": {
"id": "dev.lithe",
"displayName": "Lithe",
"signatureRequirement": "sameTeamAsHost"
},
"entrypoint": {
"kind": "nativeBundle",
"bundleIdentifier": "dev.lithe.plugin.linux-do-support.bundle",
"principalClass": "LitheLinuxDoSupportPluginEntrypoint",
"bundlePath": "LinuxDoSupport.bundle"
},
"modules": [
{
"id": "dev.lithe.community.linux-do",
"displayName": "LINUX DO",
"scope": "application",
"defaultState": "disabled",
"activationPolicy": "onDemand",
"sleepPolicy": { "kind": "never" },
"moduleDependencies": [],
"capabilityDependencies": [],
"providedCapabilities": [],
"contributions": [
{
"id": "community.linux-do",
"kind": "toolWindow",
"title": "LINUX DO",
"icon": "bubble.left.and.bubble.right",
"placement": "rightSidebar",
"order": 100,
"actionID": "community.linux-do.toggle",
"rendererID": "community.linux-do.browser",
"visibility": {}
}
],
"required": false
}
]
}
17 changes: 15 additions & 2 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Lithe</string>
<key>CFBundleDisplayName</key>
<string>Lithe</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>app.lithe.desktop.authorization</string>
<key>CFBundleURLSchemes</key>
<array>
<string>lithe</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>Lithe</string>
<key>CFBundleIconFile</key>
Expand Down
3 changes: 3 additions & 0 deletions Sources/Lithe/Application/Composition/AppServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ final class AppServices {
let githubService: GitHubService
let secureStore: any SecureStore
let databaseSecureStore: any SecureStore
let discourseCommunityService: DiscourseCommunityService
let credentialResolver: any AIProviderCredentialResolver
let aiConfigurationSources: [any AIConfigurationSource]
let recentProjectsStore: RecentProjectsStore
Expand Down Expand Up @@ -62,6 +63,7 @@ final class AppServices {
githubService: GitHubService,
secureStore: any SecureStore,
databaseSecureStore: any SecureStore,
discourseCommunityService: DiscourseCommunityService,
credentialResolver: any AIProviderCredentialResolver,
aiConfigurationSources: [any AIConfigurationSource],
recentProjectsStore: RecentProjectsStore,
Expand Down Expand Up @@ -95,6 +97,7 @@ final class AppServices {
self.githubService = githubService
self.secureStore = secureStore
self.databaseSecureStore = databaseSecureStore
self.discourseCommunityService = discourseCommunityService
self.credentialResolver = credentialResolver
self.aiConfigurationSources = aiConfigurationSources
self.recentProjectsStore = recentProjectsStore
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import Foundation

@MainActor
final class DiscourseCommunityFeatureModel: ObservableObject {
enum State: Equatable {
case signedOut
case authorizing
case loading
case ready
case failed(String)
}

enum Feed: String, CaseIterable, Identifiable {
case latest
case top

var id: String { rawValue }
}

@Published private(set) var state: State
@Published private(set) var topics: [RustCoreBridge.DiscourseTopicSummary] = []
@Published private(set) var categories: [RustCoreBridge.DiscourseCategory] = []
@Published private(set) var selectedTopic: RustCoreBridge.DiscourseTopicResponse?
@Published var selectedFeed: Feed = .latest
@Published var searchQuery = ""

private let service: DiscourseCommunityService

init(service: DiscourseCommunityService) {
self.service = service
state = service.isSignedIn ? .ready : .signedOut
service.authorizationDidComplete = { [weak self] result in
guard let self else { return }
switch result {
case .success:
Task { await self.refresh() }
case .failure(let error):
self.state = .failed(error.localizedDescription)
}
}
}

func authorize() async {
state = .authorizing
do {
try await service.beginAuthorization()
} catch {
state = .failed(error.localizedDescription)
}
}

func refresh() async {
state = .loading
do {
async let topicPage = service.topics(feed: selectedFeed.rawValue)
async let categoryPage = service.categories()
let (topicResult, categoryResult) = try await (topicPage, categoryPage)
topics = topicResult.topics
categories = categoryResult.categories
selectedTopic = nil
state = .ready
} catch {
state = .failed(error.localizedDescription)
}
}

func selectTopic(_ topic: RustCoreBridge.DiscourseTopicSummary) async {
state = .loading
do {
selectedTopic = try await service.topic(id: topic.id)
state = .ready
} catch {
state = .failed(error.localizedDescription)
}
}

func closeTopic() {
selectedTopic = nil
}

func search() async {
let query = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines)
guard !query.isEmpty else {
await refresh()
return
}
state = .loading
do {
let result = try await service.search(query: query)
topics = result.topics
selectedTopic = nil
state = .ready
} catch {
state = .failed(error.localizedDescription)
}
}

func signOut() async {
do {
try await service.signOut()
topics = []
categories = []
selectedTopic = nil
state = .signedOut
} catch {
// The service clears Keychain after every remote revoke attempt.
topics = []
selectedTopic = nil
state = .failed(error.localizedDescription)
}
}

func topicURL(id: UInt64, slug: String) -> URL? {
URL(string: "\(DiscourseCommunityService.origin)/t/\(slug)/\(id)")
}

func openTopic(id: UInt64, slug: String) {
service.openTopic(id: id, slug: slug)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

/// Routes operating-system URL callbacks to an application authorization workflow.
@MainActor
protocol ExternalAuthorizationCallbackRouting: AnyObject {
func installHandler(_ handler: @escaping @MainActor (URL) -> Void)
}
Loading
Loading