Skip to content

Commit 91cc96f

Browse files
authored
Merge pull request #1296 from DimensionDev/feature/ios_compose_ux
complete compose for ios
2 parents 2de34c9 + c6cc097 commit 91cc96f

File tree

12 files changed

+269
-42
lines changed

12 files changed

+269
-42
lines changed

compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/CommonStatusComponent.kt

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,20 +1060,22 @@ private fun StatusPollComponent(
10601060
style = PlatformTheme.typography.caption,
10611061
)
10621062
} else {
1063-
val localizedExpiredTimeline =
1064-
rememberFormattedDateTime(poll.expiredAt, fullTime = true)
1065-
PlatformText(
1066-
text =
1067-
stringResource(
1068-
resource = Res.string.poll_expired_at,
1069-
localizedExpiredTimeline,
1070-
),
1071-
modifier =
1072-
Modifier
1073-
.align(Alignment.End)
1074-
.alpha(MediumAlpha),
1075-
style = PlatformTheme.typography.caption,
1076-
)
1063+
poll.expiredAt?.let { expiredAt ->
1064+
val localizedExpiredTimeline =
1065+
rememberFormattedDateTime(expiredAt, fullTime = true)
1066+
PlatformText(
1067+
text =
1068+
stringResource(
1069+
resource = Res.string.poll_expired_at,
1070+
localizedExpiredTimeline,
1071+
),
1072+
modifier =
1073+
Modifier
1074+
.align(Alignment.End)
1075+
.alpha(MediumAlpha),
1076+
style = PlatformTheme.typography.caption,
1077+
)
1078+
}
10771079
}
10781080
if (poll.canVote) {
10791081
PlatformFilledTonalButton(

iosApp/Flare.xcodeproj/project.pbxproj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* Begin PBXBuildFile section */
1010
0646B2622E7151A700535A3E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0646B2612E7151A700535A3E /* Kingfisher */; };
11+
0672EE122E8689C70092AD1F /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 0672EE112E8689C70092AD1F /* Drops */; };
1112
068923482E82A80700981D8E /* Flow in Frameworks */ = {isa = PBXBuildFile; productRef = 068923472E82A80700981D8E /* Flow */; };
1213
068F7CD12E75405A00B5FB40 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 068F7CD02E75405A00B5FB40 /* MarkdownUI */; };
1314
06C6B5482E853AAF00CCD388 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 06C6B5472E853AAF00CCD388 /* SwiftUIIntrospect */; };
@@ -47,6 +48,7 @@
4748
068923482E82A80700981D8E /* Flow in Frameworks */,
4849
06C7FC172E7D474900A0D01A /* LazyPager in Frameworks */,
4950
068F7CD12E75405A00B5FB40 /* MarkdownUI in Frameworks */,
51+
0672EE122E8689C70092AD1F /* Drops in Frameworks */,
5052
06C6B5482E853AAF00CCD388 /* SwiftUIIntrospect in Frameworks */,
5153
0646B2622E7151A700535A3E /* Kingfisher in Frameworks */,
5254
);
@@ -97,6 +99,7 @@
9799
06C7FC162E7D474900A0D01A /* LazyPager */,
98100
068923472E82A80700981D8E /* Flow */,
99101
06C6B5472E853AAF00CCD388 /* SwiftUIIntrospect */,
102+
0672EE112E8689C70092AD1F /* Drops */,
100103
);
101104
productName = flare;
102105
productReference = 06E433FE2E6A9A2600CD0826 /* Flare.app */;
@@ -133,6 +136,7 @@
133136
06C7FC152E7D474900A0D01A /* XCRemoteSwiftPackageReference "LazyPager" */,
134137
068923462E82A80700981D8E /* XCRemoteSwiftPackageReference "SwiftUI-Flow" */,
135138
06C6B5462E853AAF00CCD388 /* XCRemoteSwiftPackageReference "swiftui-introspect" */,
139+
0672EE102E8689C70092AD1F /* XCRemoteSwiftPackageReference "Drops" */,
136140
);
137141
preferredProjectObjectVersion = 77;
138142
productRefGroup = 06E433FF2E6A9A2600CD0826 /* Products */;
@@ -421,6 +425,14 @@
421425
version = 8.5.0;
422426
};
423427
};
428+
0672EE102E8689C70092AD1F /* XCRemoteSwiftPackageReference "Drops" */ = {
429+
isa = XCRemoteSwiftPackageReference;
430+
repositoryURL = "https://github.com/omaralbeik/Drops.git";
431+
requirement = {
432+
kind = exactVersion;
433+
version = 1.7.0;
434+
};
435+
};
424436
06791BD42E7AA40000FF2050 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */ = {
425437
isa = XCRemoteSwiftPackageReference;
426438
repositoryURL = "https://github.com/SimplyDanny/SwiftLintPlugins";
@@ -469,6 +481,11 @@
469481
package = 0646B2602E7151A700535A3E /* XCRemoteSwiftPackageReference "Kingfisher" */;
470482
productName = Kingfisher;
471483
};
484+
0672EE112E8689C70092AD1F /* Drops */ = {
485+
isa = XCSwiftPackageProductDependency;
486+
package = 0672EE102E8689C70092AD1F /* XCRemoteSwiftPackageReference "Drops" */;
487+
productName = Drops;
488+
};
472489
068923472E82A80700981D8E /* Flow */ = {
473490
isa = XCSwiftPackageProductDependency;
474491
package = 068923462E82A80700981D8E /* XCRemoteSwiftPackageReference "SwiftUI-Flow" */;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Foundation
2+
import KotlinSharedUI
3+
4+
extension KotlinByteArray {
5+
static func from(data: Data) -> KotlinByteArray {
6+
let swiftByteArray = [UInt8](data)
7+
return swiftByteArray
8+
.map(Int8.init(bitPattern:))
9+
.enumerated()
10+
.reduce(into: KotlinByteArray(size: Int32(swiftByteArray.count))) { result, row in
11+
result.set(index: Int32(row.offset), value: row.element)
12+
}
13+
}
14+
}

iosApp/flare/FlareApp.swift

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,11 @@ import KotlinSharedUI
44
@main
55
struct FlareApp: App {
66
init() {
7-
ComposeUIHelper.shared.initialize(inAppNotification: SwiftInAppNotification())
7+
ComposeUIHelper.shared.initialize(inAppNotification: SwiftInAppNotification.shared)
88
}
99
var body: some Scene {
1010
WindowGroup {
1111
FlareRoot()
1212
}
1313
}
1414
}
15-
16-
class SwiftInAppNotification: InAppNotification {
17-
func onError(message: Message, throwable: KotlinThrowable) {
18-
19-
}
20-
21-
func onProgress(message: Message, progress: Int32, total: Int32) {
22-
23-
}
24-
25-
func onSuccess(message: Message) {
26-
27-
}
28-
}

iosApp/flare/Localizable.xcstrings

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,9 +876,27 @@
876876
},
877877
"Not done yet for %@" : {
878878

879+
},
880+
"notification_compose_error" : {
881+
882+
},
883+
"notification_compose_success" : {
884+
885+
},
886+
"notification_login_expired" : {
887+
879888
},
880889
"notification_type_title" : {
881890

891+
},
892+
"poll_expired" : {
893+
894+
},
895+
"poll_expires_at" : {
896+
897+
},
898+
"poll_vote" : {
899+
882900
},
883901
"profile_tab_likes" : {
884902

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import SwiftUI
2+
import KotlinSharedUI
3+
4+
struct StatusPollView: View {
5+
let data: UiPoll
6+
@State private var selectedOption: [Int] = []
7+
var body: some View {
8+
VStack(
9+
alignment: .trailing
10+
) {
11+
ForEach(0..<data.options.count, id: \.self) { index in
12+
let option = data.options[index]
13+
if data.canVote {
14+
Button {
15+
if data.multiple {
16+
if selectedOption.contains(index) {
17+
selectedOption.removeAll(where: { $0 == index })
18+
} else {
19+
selectedOption.append(index)
20+
}
21+
} else {
22+
selectedOption = [index]
23+
}
24+
} label: {
25+
HStack {
26+
Text(option.title)
27+
.font(.caption)
28+
Spacer()
29+
if selectedOption.contains(index) {
30+
Image(systemName: "checkmark.circle")
31+
.font(.caption)
32+
.foregroundStyle(.secondary)
33+
} else {
34+
EmptyView()
35+
}
36+
}
37+
.padding(8)
38+
.frame(maxWidth: .infinity)
39+
.background(
40+
RoundedRectangle(cornerRadius: 8, style: .continuous)
41+
.fill(selectedOption.contains(index) ? Color.accentColor.opacity(0.2) : Color(.systemGroupedBackground))
42+
)
43+
}
44+
.buttonStyle(.plain)
45+
} else {
46+
VStack {
47+
HStack {
48+
Text(option.title)
49+
.font(.caption)
50+
Spacer()
51+
if data.ownVotes.contains(KotlinInt(value: Int32(index))) {
52+
Image(systemName: "checkmark.circle")
53+
.font(.caption)
54+
} else {
55+
EmptyView()
56+
}
57+
Text(option.humanizedPercentage)
58+
.font(.caption)
59+
.foregroundStyle(.secondary)
60+
}
61+
ProgressView(value: option.percentage)
62+
.progressViewStyle(.linear)
63+
.tint(.accentColor)
64+
}
65+
.frame(maxWidth: .infinity)
66+
}
67+
}
68+
69+
if data.expired {
70+
Text("poll_expired")
71+
.font(.caption)
72+
.foregroundStyle(.secondary)
73+
} else if let expiredAt = data.expiredAt {
74+
Text("poll_expires_at")
75+
.font(.caption)
76+
.foregroundStyle(.secondary)
77+
DateTimeText(data: expiredAt, fullTime: true)
78+
.font(.caption)
79+
.foregroundStyle(.secondary)
80+
}
81+
82+
if data.canVote {
83+
Button {
84+
data.onVote(selectedOption.map { KotlinInt(value: Int32($0)) } )
85+
} label: {
86+
Text("poll_vote")
87+
}
88+
.buttonStyle(.glassProminent)
89+
}
90+
}
91+
}
92+
}
93+

iosApp/flare/UI/Component/Status/StatusView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ struct StatusView: View {
120120

121121
}
122122
}
123+
if let poll = data.poll, showMedia {
124+
StatusPollView(data: poll)
125+
}
126+
123127
if !data.images.isEmpty, showMedia {
124128
StatusMediaView(data: data.images, sensitive: data.sensitive)
125129
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import KotlinSharedUI
2+
import Drops
3+
import Foundation
4+
5+
class SwiftInAppNotification: InAppNotification {
6+
private init() {}
7+
static let shared = SwiftInAppNotification()
8+
9+
func onError(message: Message, throwable: KotlinThrowable) {
10+
switch message {
11+
case .compose:
12+
Drops.show(.init(stringLiteral: .init(localized: "notification_compose_error")))
13+
case .loginExpired:
14+
Drops.show(.init(stringLiteral: .init(localized: "notification_login_expired")))
15+
}
16+
}
17+
18+
func onProgress(message: Message, progress: Int32, total: Int32) {
19+
20+
}
21+
22+
func onSuccess(message: Message) {
23+
switch message {
24+
case .compose:
25+
Drops.show(.init(stringLiteral: .init(localized: "notification_compose_success")))
26+
case .loginExpired:
27+
// do nothing
28+
break
29+
}
30+
}
31+
}

iosApp/flare/UI/Screen/ComposeScreen.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ struct ComposeScreen: View {
320320
}
321321
ToolbarItem(placement: .confirmationAction) {
322322
Button {
323+
send()
323324
} label: {
324325
Label {
325326
Text("compose_button_send")
@@ -358,6 +359,54 @@ struct ComposeScreen: View {
358359
textView.selectedRange = NSRange(location: newLocation, length: 0)
359360
textView.scrollRangeToVisible(NSRange(location: max(0, newLocation - 1), length: 1))
360361
}
362+
363+
private func getComposeData() -> [ComposeData] {
364+
return presenter.state.selectedAccounts.map { account in
365+
ComposeData(
366+
account: account,
367+
content: viewModel.text,
368+
visibility: getVisibility(),
369+
language: ["en"],
370+
medias: getMedia(),
371+
sensitive: viewModel.mediaViewModel.sensitive,
372+
spoilerText: viewModel.contentWarning,
373+
poll: getPoll(),
374+
localOnly: false,
375+
referenceStatus: getReferenceStatus()
376+
)
377+
}
378+
}
379+
380+
private func send() {
381+
let datas = getComposeData()
382+
for data in datas {
383+
presenter.state.send(data: data)
384+
}
385+
dismiss()
386+
}
387+
388+
private func getMedia() -> [FileItem] {
389+
return viewModel.mediaViewModel.items.map { item in
390+
FileItem(name: item.item.itemIdentifier, data: KotlinByteArray.from(data: item.data!))
391+
}
392+
}
393+
private func getReferenceStatus() -> ComposeData.ReferenceStatus? {
394+
return if let data = composeStatus, let replyState = presenter.state.replyState, case .success(let timeline) = onEnum(of: replyState) {
395+
ComposeData.ReferenceStatus(data: timeline.data, composeStatus: data)
396+
} else {
397+
nil
398+
}
399+
}
400+
private func getPoll() -> ComposeData.Poll? {
401+
return if viewModel.pollViewModel.enabled {
402+
ComposeData.Poll(options: viewModel.pollViewModel.choices.map { item in item.text }, expiredAfter: viewModel.pollViewModel.expired.inWholeMilliseconds, multiple: viewModel.pollViewModel.pollType == ComposePollType.multiple)
403+
} else {
404+
nil
405+
}
406+
}
407+
private func getVisibility() -> UiTimeline.ItemContentStatusTopEndContentVisibilityType {
408+
return viewModel.visibility
409+
}
361410

362411
}
363412

shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiPoll.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,19 @@ public data class UiPoll internal constructor(
1515
val multiple: Boolean,
1616
val ownVotes: ImmutableList<Int>,
1717
val onVote: (options: ImmutableList<Int>) -> Unit,
18-
private val expiresAt: Instant,
18+
// null indicates no expiration
19+
private val expiresAt: Instant?,
1920
private val enabled: Boolean = true,
2021
) {
21-
val expired: Boolean by lazy { expiresAt < Clock.System.now() }
22+
val expired: Boolean by lazy {
23+
if (expiresAt == null) {
24+
false
25+
} else {
26+
expiresAt < Clock.System.now()
27+
}
28+
}
2229
val voted: Boolean by lazy { ownVotes.isNotEmpty() }
23-
val expiredAt: UiDateTime by lazy { expiresAt.toUi() }
30+
val expiredAt: UiDateTime? by lazy { expiresAt?.toUi() }
2431

2532
val canVote: Boolean by lazy { !expired && !voted && enabled }
2633

0 commit comments

Comments
 (0)