Skip to content

Commit 82f87db

Browse files
authored
Merge pull request #1612 from DimensionDev/feature/compose_overhaul
compose overhaul
2 parents 4468861 + 535f244 commit 82f87db

File tree

18 files changed

+2943
-1976
lines changed

18 files changed

+2943
-1976
lines changed

app/src/main/java/dev/dimension/flare/ui/screen/compose/ComposeScreen.kt

Lines changed: 269 additions & 27 deletions
Large diffs are not rendered by default.

desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/compose/ComposeDialog.kt

Lines changed: 198 additions & 32 deletions
Large diffs are not rendered by default.

iosApp/flare/Localizable.xcstrings

Lines changed: 1872 additions & 1842 deletions
Large diffs are not rendered by default.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import SwiftUI
2+
import PhotosUI
3+
4+
struct AltTextEditSheet: View {
5+
@Environment(\.dismiss) var dismiss
6+
@Bindable var item: MediaItem
7+
let maxLength: Int
8+
9+
var body: some View {
10+
NavigationStack {
11+
VStack {
12+
if let image = item.image {
13+
Image(uiImage: image)
14+
.resizable()
15+
.scaledToFit()
16+
.frame(maxHeight: 300)
17+
.clipShape(RoundedRectangle(cornerRadius: 8))
18+
}
19+
20+
TextField("Description", text: $item.altText, axis: .vertical)
21+
.textFieldStyle(.roundedBorder)
22+
.lineLimit(3...10)
23+
.padding()
24+
.onChange(of: item.altText) { oldValue, newValue in
25+
if newValue.count > maxLength {
26+
item.altText = String(newValue.prefix(maxLength))
27+
}
28+
}
29+
30+
HStack {
31+
Spacer()
32+
Text("\(item.altText.count)/\(maxLength)")
33+
.font(.caption)
34+
.foregroundStyle(.secondary)
35+
}
36+
.padding(.horizontal)
37+
38+
Spacer()
39+
}
40+
.padding()
41+
.navigationTitle("Edit Description")
42+
.navigationBarTitleDisplayMode(.inline)
43+
.toolbar {
44+
ToolbarItem(placement: .confirmationAction) {
45+
Button("Done") {
46+
dismiss()
47+
}
48+
}
49+
}
50+
}
51+
}
52+
}

iosApp/flare/UI/Screen/ComposeScreen.swift

Lines changed: 144 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -111,32 +111,17 @@ struct ComposeScreen: View {
111111
ScrollView(.horizontal) {
112112
HStack {
113113
ForEach(viewModel.mediaViewModel.items) { item in
114-
if let image = item.image {
115-
Image(uiImage: image)
116-
.resizable()
117-
.scaledToFill()
118-
.frame(width: 128, height: 128)
119-
.cornerRadius(8)
120-
.contextMenu {
121-
Button(action: {
122-
withAnimation {
123-
viewModel.mediaViewModel.remove(item: item)
124-
}
125-
}, label: {
126-
Label {
127-
Text("delete")
128-
} icon: {
129-
Image("fa-trash")
130-
}
131-
})
132-
}
133-
}
114+
ComposeMediaItemView(item: item, mediaViewModel: viewModel.mediaViewModel)
134115
}
135116
}
136117
}
137-
Toggle(isOn: $viewModel.mediaViewModel.sensitive, label: {
138-
Text("compose_media_mark_sensitive")
139-
})
118+
StateView(state: presenter.state.composeConfig) { config in
119+
if let media = config.media, media.canSensitive {
120+
Toggle(isOn: $viewModel.mediaViewModel.sensitive, label: {
121+
Text("compose_media_mark_sensitive")
122+
})
123+
}
124+
}
140125
}
141126
if viewModel.pollViewModel.enabled {
142127
HStack(
@@ -214,13 +199,16 @@ struct ComposeScreen: View {
214199
) {
215200
if !viewModel.pollViewModel.enabled {
216201
StateView(state: presenter.state.composeConfig) { config in
217-
if let media = config.media {
218-
PhotosPicker(selection: Binding(get: {
219-
viewModel.mediaViewModel.selectedItems
220-
}, set: { value in
221-
viewModel.mediaViewModel.selectedItems = value
222-
viewModel.mediaViewModel.update()
223-
}), matching: .any(of: [.images, .videos, .livePhotos])) {
202+
if config.media != nil {
203+
PhotosPicker(
204+
selection: Binding(get: {
205+
viewModel.mediaViewModel.selectedItems
206+
}, set: { value in
207+
viewModel.mediaViewModel.selectedItems = value
208+
viewModel.mediaViewModel.update()
209+
}),
210+
maxSelectionCount: viewModel.mediaViewModel.maxSize,
211+
matching: .any(of: [.images, .videos, .livePhotos])) {
224212
Image("fa-image")
225213
}
226214
}
@@ -303,11 +291,54 @@ struct ComposeScreen: View {
303291
}
304292
}
305293
}
294+
StateView(state: presenter.state.composeConfig) { config in
295+
if let languageConfig = config.language {
296+
Menu {
297+
ForEach(languageConfig.sortedIsoCodes, id: \.self) { code in
298+
Button {
299+
if languageConfig.maxCount > 1 {
300+
if viewModel.languages.contains(code) {
301+
if viewModel.languages.count > 1 {
302+
viewModel.languages.removeAll { $0 == code }
303+
}
304+
} else {
305+
if viewModel.languages.count < languageConfig.maxCount {
306+
viewModel.languages.append(code)
307+
}
308+
}
309+
} else {
310+
viewModel.languages = [code]
311+
}
312+
} label: {
313+
HStack {
314+
Text(Locale.current.localizedString(forLanguageCode: code) ?? code)
315+
if viewModel.languages.contains(code) {
316+
Image(systemName: "checkmark")
317+
}
318+
}
319+
}
320+
}
321+
} label: {
322+
if let first = viewModel.languages.first, viewModel.languages.count == 1 {
323+
Text(first.uppercased())
324+
.font(.caption)
325+
.bold()
326+
.padding(4)
327+
.overlay(
328+
RoundedRectangle(cornerRadius: 4)
329+
.stroke(Color.primary, lineWidth: 1)
330+
)
331+
} else {
332+
Image(systemName: "globe")
333+
}
334+
}
335+
}
336+
}
306337
}
307-
.scrollIndicators(.hidden)
308338
.font(.title)
309339
.buttonStyle(.plain)
310340
}
341+
.scrollIndicators(.hidden)
311342
Spacer()
312343
StateView(state: presenter.state.composeConfig) { config in
313344
if let maxLength = config.text?.maxLength {
@@ -333,8 +364,16 @@ struct ComposeScreen: View {
333364
.onSuccessOf(of: presenter.state.composeConfig) { config in
334365
if let media = config.media {
335366
viewModel.mediaViewModel.maxSize = Int(media.maxCount)
367+
viewModel.mediaViewModel.enableAltText = media.altTextMaxLength > 0
368+
viewModel.mediaViewModel.altTextMaxLength = Int(media.altTextMaxLength)
336369
}
337370
}
371+
.onChange(of: viewModel.text) { oldValue, newValue in
372+
presenter.state.setText(value: newValue)
373+
}
374+
.onChange(of: viewModel.mediaViewModel.items.count) { _, newValue in
375+
presenter.state.setMediaSize(value: Int32(newValue))
376+
}
338377
.toolbarTitleDisplayMode(.inline)
339378
.toolbar {
340379
ToolbarItem(placement: .principal) {
@@ -368,7 +407,7 @@ struct ComposeScreen: View {
368407
Image(systemName: "paperplane.fill")
369408
}
370409
}
371-
.disabled(viewModel.text.isEmpty)
410+
.disabled(!presenter.state.canSend)
372411
}
373412
}
374413
}
@@ -407,7 +446,7 @@ struct ComposeScreen: View {
407446
account: account,
408447
content: viewModel.text,
409448
visibility: getVisibility(),
410-
language: ["en"],
449+
language: viewModel.languages,
411450
medias: getMedia(),
412451
sensitive: viewModel.mediaViewModel.sensitive,
413452
spoilerText: viewModel.contentWarning,
@@ -426,9 +465,9 @@ struct ComposeScreen: View {
426465
dismiss()
427466
}
428467

429-
private func getMedia() -> [FileItem] {
468+
private func getMedia() -> [ComposeData.Media] {
430469
return viewModel.mediaViewModel.items.map { item in
431-
FileItem(name: item.item.itemIdentifier, data: KotlinByteArray.from(data: item.data!))
470+
.init(file: .init(name: item.item.itemIdentifier, data: KotlinByteArray.from(data: item.data!)), altText: item.altText.isEmpty ? nil : item.altText)
432471
}
433472
}
434473
private func getReferenceStatus() -> ComposeData.ReferenceStatus? {
@@ -460,6 +499,12 @@ class ComposeInputViewModel {
460499
var pollViewModel = PollViewModel()
461500
var mediaViewModel = MediaViewModel()
462501
var visibility: UiTimeline.ItemContentStatusTopEndContentVisibilityType = .public
502+
var languages: [String] = {
503+
if let code = Locale.current.language.languageCode?.identifier {
504+
return [code]
505+
}
506+
return ["en"]
507+
}()
463508

464509

465510
func showEmojiPanel() {
@@ -488,6 +533,8 @@ class MediaViewModel {
488533
var items: [MediaItem] = []
489534
var sensitive = false
490535
var maxSize = 4
536+
var enableAltText = true
537+
var altTextMaxLength = 500
491538
func update() {
492539
if selectedItems.count > maxSize {
493540
selectedItems = Array(selectedItems[(selectedItems.count - 4)...(selectedItems.count - 1)])
@@ -514,11 +561,12 @@ class MediaItem: Equatable, Identifiable {
514561
let item: PhotosPickerItem
515562
var image: UIImage?
516563
var data: Data?
517-
var id: String {
518-
item.itemIdentifier ?? UUID().uuidString
519-
}
564+
var altText: String = ""
565+
let id: String
566+
520567
init(item: PhotosPickerItem) {
521568
self.item = item
569+
self.id = item.itemIdentifier ?? UUID().uuidString
522570
item.loadTransferable(type: Data.self) { result in
523571
do {
524572
if let data = try result.get() {
@@ -614,3 +662,61 @@ extension ComposeScreen {
614662
self._presenter = .init(wrappedValue: .init(presenter: ComposePresenter(accountType: accountType, status: composeStatus)))
615663
}
616664
}
665+
666+
struct ComposeMediaItemView: View {
667+
let item: MediaItem
668+
var mediaViewModel: MediaViewModel
669+
@State private var showAltTextEditor = false
670+
671+
var body: some View {
672+
if let image = item.image {
673+
Image(uiImage: image)
674+
.resizable()
675+
.scaledToFill()
676+
.frame(width: 128, height: 128)
677+
.cornerRadius(8)
678+
.overlay(alignment: .bottomLeading) {
679+
if mediaViewModel.enableAltText && !item.altText.isEmpty {
680+
Text("ALT")
681+
.font(.caption2)
682+
.bold()
683+
.foregroundStyle(.black)
684+
.padding(.horizontal, 4)
685+
.padding(.vertical, 2)
686+
.background(.white.opacity(0.8))
687+
.clipShape(RoundedRectangle(cornerRadius: 4))
688+
.padding(4)
689+
}
690+
}
691+
.onTapGesture {
692+
if mediaViewModel.enableAltText {
693+
showAltTextEditor = true
694+
}
695+
}
696+
.contextMenu {
697+
Button(action: {
698+
withAnimation {
699+
mediaViewModel.remove(item: item)
700+
}
701+
}, label: {
702+
Label {
703+
Text("delete")
704+
} icon: {
705+
Image("fa-trash")
706+
}
707+
})
708+
709+
if mediaViewModel.enableAltText {
710+
Button {
711+
showAltTextEditor = true
712+
} label: {
713+
Label("Edit Description", systemImage: "pencil")
714+
}
715+
}
716+
}
717+
.sheet(isPresented: $showAltTextEditor) {
718+
AltTextEditSheet(item: item, maxLength: mediaViewModel.altTextMaxLength)
719+
}
720+
}
721+
}
722+
}

shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import dev.dimension.flare.data.datasource.microblog.AuthenticatedMicroblogDataS
7979
import dev.dimension.flare.data.datasource.microblog.ComposeConfig
8080
import dev.dimension.flare.data.datasource.microblog.ComposeData
8181
import dev.dimension.flare.data.datasource.microblog.ComposeProgress
82+
import dev.dimension.flare.data.datasource.microblog.ComposeType
8283
import dev.dimension.flare.data.datasource.microblog.DirectMessageDataSource
8384
import dev.dimension.flare.data.datasource.microblog.ListDataSource
8485
import dev.dimension.flare.data.datasource.microblog.ListMetaData
@@ -136,6 +137,7 @@ import sh.christian.ozone.api.AtUri
136137
import sh.christian.ozone.api.Cid
137138
import sh.christian.ozone.api.Did
138139
import sh.christian.ozone.api.Handle
140+
import sh.christian.ozone.api.Language
139141
import sh.christian.ozone.api.Nsid
140142
import sh.christian.ozone.api.RKey
141143
import sh.christian.ozone.api.model.JsonContent
@@ -361,14 +363,15 @@ internal class BlueskyDataSource(
361363
val maxProgress = data.medias.size + 1
362364
val mediaBlob =
363365
data.medias
364-
.mapIndexedNotNull { index, item ->
366+
.mapIndexedNotNull { index, (item, altText) ->
365367
service
366368
.uploadBlob(item.readBytes())
367369
.also {
368370
progress(ComposeProgress(index + 1, maxProgress))
369371
}.maybeResponse()
370-
}.map {
371-
it.blob
372+
?.let {
373+
it.blob to altText
374+
}
372375
}
373376
val facets =
374377
parseBskyFacets(
@@ -407,7 +410,7 @@ internal class BlueskyDataSource(
407410
Images(
408411
blobs
409412
.map { blob ->
410-
ImagesImage(image = blob, alt = "")
413+
ImagesImage(image = blob.first, alt = blob.second.orEmpty())
411414
}.toImmutableList(),
412415
),
413416
)
@@ -441,6 +444,10 @@ internal class BlueskyDataSource(
441444
root = root,
442445
)
443446
},
447+
langs =
448+
data.language.map {
449+
Language(it)
450+
},
444451
)
445452
service
446453
.createRecord(
@@ -972,10 +979,17 @@ internal class BlueskyDataSource(
972979

973980
override fun discoverStatuses() = throw UnsupportedOperationException("Bluesky does not support discover statuses")
974981

975-
override fun composeConfig(statusKey: MicroBlogKey?): ComposeConfig =
982+
override fun composeConfig(type: ComposeType): ComposeConfig =
976983
ComposeConfig(
977984
text = ComposeConfig.Text(300),
978-
media = ComposeConfig.Media(4, true),
985+
media =
986+
ComposeConfig.Media(
987+
maxCount = 4,
988+
canSensitive = true,
989+
altTextMaxLength = 2000,
990+
allowMediaOnly = true,
991+
),
992+
language = ComposeConfig.Language(3),
979993
)
980994

981995
override suspend fun follow(

0 commit comments

Comments
 (0)