Skip to content

Commit fd48336

Browse files
committed
add deeplink handle for ios
1 parent 110120f commit fd48336

File tree

6 files changed

+203
-33
lines changed

6 files changed

+203
-33
lines changed

iosApp/flare/Localizable.xcstrings

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18270,6 +18270,26 @@
1827018270
}
1827118271
}
1827218272
},
18273+
"deep_link_account_picker_open_in_browser" : {
18274+
"localizations" : {
18275+
"en" : {
18276+
"stringUnit" : {
18277+
"state" : "translated",
18278+
"value" : "Open in browser"
18279+
}
18280+
}
18281+
}
18282+
},
18283+
"deep_link_account_picker_title" : {
18284+
"localizations" : {
18285+
"en" : {
18286+
"stringUnit" : {
18287+
"state" : "translated",
18288+
"value" : "Select account"
18289+
}
18290+
}
18291+
}
18292+
},
1827318293
"delete" : {
1827418294
"localizations" : {
1827518295
"af" : {

iosApp/flare/UI/Component/Backport.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ extension Backport where Content: View {
1010
content.labelStyle(BackportLabelStyle(spacing: spacing))
1111
}
1212
}
13+
14+
@ViewBuilder
15+
func navigationSubtitle(_ subtitle: Text) -> some View {
16+
if #available(iOS 26.0, *) {
17+
content.navigationSubtitle(subtitle)
18+
} else {
19+
content
20+
}
21+
}
22+
23+
@ViewBuilder
24+
func navigationSubtitle<S>(_ subtitle: S) -> some View where S : StringProtocol {
25+
if #available(iOS 26.0, *) {
26+
content.navigationSubtitle(subtitle)
27+
} else {
28+
content
29+
}
30+
}
1331
}
1432

1533
struct BackportLabelStyle: LabelStyle {

iosApp/flare/UI/Route/Route.swift

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ enum Route: Hashable, Identifiable {
9090
StatusMediaScreen(accountType: accountType, statusKey: statusKey, initialIndex: Int(selectedIndex), preview: preview)
9191
case .appLog:
9292
AppLogScreen()
93+
case .deepLinkAccountPicker(let originalUrl, let data):
94+
DeepLinkAccountPicker(originalUrl: originalUrl, data: data, onNavigate: onNavigate)
9395
default:
9496
Text("Not done yet for \(self)")
9597
}
@@ -133,6 +135,7 @@ enum Route: Hashable, Identifiable {
133135
case userFans(AccountType, MicroBlogKey)
134136
case dmConversation(AccountType, MicroBlogKey, String)
135137
case appLog
138+
case deepLinkAccountPicker(String, [MicroBlogKey: Route])
136139

137140
fileprivate static func fromCompose(_ compose: DeeplinkRoute.Compose) -> Route? {
138141
switch onEnum(of: compose) {
@@ -192,28 +195,37 @@ enum Route: Hashable, Identifiable {
192195

193196
static func fromDeepLink(url: String) -> Route? {
194197
if let deeplinkRoute = DeeplinkRoute.companion.parse(url: url) {
195-
switch onEnum(of: deeplinkRoute) {
196-
case .compose(let compose):
197-
return fromCompose(compose)
198-
case .media(let media):
199-
return fromMedia(media)
200-
case .profile(let profile):
201-
return fromProfile(profile)
202-
case .rss(let rss):
203-
switch onEnum(of: rss) {
204-
case .detail(let data):
205-
return Route.rssDetail(data.url)
206-
}
207-
case .search(let search):
208-
return Route.search(search.accountType, search.query)
209-
case .status(let status):
210-
return fromStatus(status)
211-
case .login:
212-
return Route.serviceSelect
213-
}
198+
return fromDeepLinkRoute(deeplinkRoute: deeplinkRoute)
214199
} else {
215200
return nil
216201
}
217-
202+
}
203+
204+
static func fromDeepLinkRoute(deeplinkRoute: DeeplinkRoute) -> Route? {
205+
switch onEnum(of: deeplinkRoute) {
206+
case .compose(let compose):
207+
return fromCompose(compose)
208+
case .media(let media):
209+
return fromMedia(media)
210+
case .profile(let profile):
211+
return fromProfile(profile)
212+
case .rss(let rss):
213+
switch onEnum(of: rss) {
214+
case .detail(let data):
215+
return Route.rssDetail(data.url)
216+
}
217+
case .search(let search):
218+
return Route.search(search.accountType, search.query)
219+
case .status(let status):
220+
return fromStatus(status)
221+
case .login:
222+
return Route.serviceSelect
223+
case .deepLinkAccountPicker(let picker):
224+
return .deepLinkAccountPicker(picker.originalUrl, picker.data.mapValues { route in
225+
return fromDeepLinkRoute(deeplinkRoute: route)
226+
}.compactMapValues { $0 })
227+
case .openLinkDirectly(_):
228+
return nil
229+
}
218230
}
219231
}

iosApp/flare/UI/Route/Router.swift

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import SwiftUI
22
import KotlinSharedUI
33
import LazyPager
4+
import Combine
45

56
struct Router<Root: View>: View {
7+
@Environment(\.openURL) private var openURL
68
@ViewBuilder let root: (@escaping (Route) -> Void) -> Root
79
@State private var backStack: [Route] = []
810
@State private var sheet: Route? = nil
@@ -11,6 +13,22 @@ struct Router<Root: View>: View {
1113
@State private var showDeleteStatusAlert = false
1214
@State private var showMastodonReportStatusAlert = false
1315
@State private var mastodonReportStatusData: (AccountType, MicroBlogKey, MicroBlogKey?)? = nil
16+
@StateObject private var deepLinkPresenter: KotlinPresenter<DeepLinkPresenterState>
17+
@StateObject private var deepLinkHandler = DeepLinkHandler()
18+
19+
init(@ViewBuilder root: @escaping (@escaping (Route) -> Void) -> Root) {
20+
self.root = root
21+
let handler = DeepLinkHandler()
22+
self._deepLinkHandler = .init(wrappedValue: handler)
23+
self._deepLinkPresenter = .init(wrappedValue: .init(presenter: DeepLinkPresenter(onRoute: { [weak handler] deeplinkRoute in
24+
if let route = Route.fromDeepLinkRoute(deeplinkRoute: deeplinkRoute){
25+
handler?.onRoute?(route)
26+
}
27+
}, onLink: { [weak handler] link in
28+
handler?.onLink?(link)
29+
})))
30+
}
31+
1432
var body: some View {
1533
NavigationStack(path: $backStack) {
1634
root({ route in
@@ -58,13 +76,19 @@ struct Router<Root: View>: View {
5876
Text("mastodon_report_status_alert_message")
5977
}
6078
.environment(\.openURL, OpenURLAction { url in
61-
if let newRoute = Route.fromDeepLink(url: url.absoluteString) {
62-
navigate(route: newRoute)
63-
return .handled
64-
} else {
65-
return .systemAction
66-
}
79+
deepLinkPresenter.state.handle(url: url.absoluteString)
80+
return .handled
6781
})
82+
.onAppear {
83+
deepLinkHandler.onRoute = { route in
84+
navigate(route: route)
85+
}
86+
deepLinkHandler.onLink = { link in
87+
if let url = URL(string: link) {
88+
openURL(url)
89+
}
90+
}
91+
}
6892
}
6993

7094
func navigate(route: Route) {
@@ -87,7 +111,15 @@ struct Router<Root: View>: View {
87111

88112
func isSheetRoute(route: Route) -> Bool {
89113
switch route {
90-
case .composeNew, .composeQuote, .composeReply, .composeVVOReplyComment, .tabSettings, .statusBlueskyReport, .statusMisskeyReport, .statusAddReaction:
114+
case .deepLinkAccountPicker,
115+
.composeNew,
116+
.composeQuote,
117+
.composeReply,
118+
.composeVVOReplyComment,
119+
.tabSettings,
120+
.statusBlueskyReport,
121+
.statusMisskeyReport,
122+
.statusAddReaction:
91123
return true
92124
default:
93125
return false
@@ -103,3 +135,8 @@ struct Router<Root: View>: View {
103135
}
104136
}
105137
}
138+
139+
class DeepLinkHandler : ObservableObject {
140+
var onRoute: ((Route) -> Void)?
141+
var onLink: ((String) -> Void)?
142+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import SwiftUI
2+
import KotlinSharedUI
3+
import SwiftUIBackports
4+
5+
struct DeepLinkAccountPicker: View {
6+
@Environment(\.dismiss) private var dismiss
7+
@Environment(\.openURL) private var openURL
8+
let originalUrl: String
9+
let data: [MicroBlogKey : Route]
10+
let onNavigate: (Route) -> Void
11+
12+
var body: some View {
13+
List {
14+
ForEach(data.keys.sorted(by: { $0.id < $1.id }), id: \.self) { userKey in
15+
if let route = data[userKey] {
16+
Button {
17+
onNavigate(route)
18+
dismiss()
19+
} label: {
20+
UserItemView(userKey: userKey)
21+
}
22+
}
23+
}
24+
Button {
25+
openURL(URL(string: originalUrl)!)
26+
dismiss()
27+
} label: {
28+
Label {
29+
Text("deep_link_account_picker_open_in_browser")
30+
} icon: {
31+
Image(.faGlobe)
32+
}
33+
34+
}
35+
}
36+
.navigationTitle("deep_link_account_picker_title")
37+
.backport
38+
.navigationSubtitle("deep_link_account_picker_subtitle")
39+
.toolbar {
40+
ToolbarItem(placement: .cancellationAction) {
41+
Button(
42+
role: .cancel
43+
) {
44+
dismiss()
45+
} label: {
46+
Label {
47+
Text("Cancel")
48+
} icon: {
49+
Image("fa-xmark")
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
57+
private struct UserItemView : View {
58+
@StateObject private var presenter: KotlinPresenter<UserState>
59+
60+
init(userKey: MicroBlogKey) {
61+
self._presenter = .init(wrappedValue: .init(presenter: UserPresenter(accountType: AccountType.Specific(accountKey: userKey), userKey: nil)))
62+
}
63+
64+
var body: some View {
65+
StateView(state: presenter.state.user) { user in
66+
UserCompatView(data: user)
67+
} errorContent: { error in
68+
UserErrorView(error: error)
69+
} loadingContent: {
70+
UserLoadingView()
71+
}
72+
}
73+
}

shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/DeepLinkPresenter.kt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import io.ktor.http.URLProtocol
1515
import io.ktor.http.buildUrl
1616
import kotlinx.collections.immutable.toImmutableList
1717
import kotlinx.collections.immutable.toImmutableMap
18+
import kotlinx.coroutines.Dispatchers
1819
import kotlinx.coroutines.flow.map
20+
import kotlinx.coroutines.withContext
1921
import org.koin.core.component.KoinComponent
2022
import org.koin.core.component.inject
2123

@@ -51,19 +53,25 @@ public class DeepLinkPresenter(
5153
if (url.startsWith("$APPSCHEMA://")) {
5254
DeeplinkRoute.parse(url)?.let {
5355
if (it is DeeplinkRoute.OpenLinkDirectly) {
54-
onLink(it.url)
56+
withContext(Dispatchers.Main) {
57+
onLink(it.url)
58+
}
5559
} else {
56-
onRoute(it)
60+
withContext(Dispatchers.Main) {
61+
onRoute(it)
62+
}
5763
}
5864
}
5965
pendingUrl = null
6066
} else {
6167
patternFlow.collect { pattern ->
6268
val matches = DeepLinkMapping.matches(url, pattern)
6369
if (matches.isEmpty()) {
64-
onLink.invoke(url)
70+
withContext(Dispatchers.Main) {
71+
onLink.invoke(url)
72+
}
6573
} else {
66-
onRoute.invoke(
74+
val route =
6775
DeeplinkRoute.DeepLinkAccountPicker(
6876
originalUrl =
6977
buildUrl {
@@ -77,8 +85,10 @@ public class DeepLinkPresenter(
7785
it.key.accountKey to it.value.deepLink(it.key.accountKey)
7886
}.toMap()
7987
.toImmutableMap(),
80-
),
81-
)
88+
)
89+
withContext(Dispatchers.Main) {
90+
onRoute.invoke(route)
91+
}
8292
}
8393
pendingUrl = null
8494
}

0 commit comments

Comments
 (0)