Skip to content

Commit 3d39ab8

Browse files
authored
feat(Changelog): modernize Changelog using Jetpack Compose (#10691)
2 parents 558960d + 0aad548 commit 3d39ab8

9 files changed

Lines changed: 590 additions & 185 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package com.fsck.k9.ui.changelog
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.ui.tooling.preview.PreviewLightDark
5+
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark
6+
import kotlinx.collections.immutable.persistentListOf
7+
8+
@Composable
9+
@PreviewLightDark
10+
fun ChangelogScreenPreview() {
11+
PreviewWithThemesLightDark {
12+
ChangelogScreen(
13+
releaseItems = releases,
14+
showRecentChanges = true,
15+
onShowRecentChangesCheck = {},
16+
)
17+
}
18+
}
19+
20+
val releases = persistentListOf(
21+
22+
ReleaseUiModel(
23+
version = "18.0b3",
24+
date = "2026-03-31",
25+
isLatest = true,
26+
changes = mapOf(
27+
ChangeType.FIXED.name to listOf(
28+
"Crash occurred when retrieving images from large messages",
29+
),
30+
),
31+
),
32+
33+
ReleaseUiModel(
34+
version = "18.0b2",
35+
date = "2026-03-18",
36+
isLatest = false,
37+
changes = mapOf(
38+
ChangeType.FIXED.name to listOf(
39+
"Crash occurred when attaching attachment",
40+
),
41+
),
42+
),
43+
44+
ReleaseUiModel(
45+
version = "18.0b1",
46+
date = "2026-03-16",
47+
isLatest = false,
48+
changes = mapOf(
49+
ChangeType.NEW.name to listOf(
50+
"Add support for PNG avatars",
51+
"Add setting to display ISO date and time in message list",
52+
"Used fixed-width font for composing when user chooses fixed-width for viewing",
53+
),
54+
ChangeType.CHANGED.name to listOf(
55+
"Attachment summary moved from message bottom to header for visibility",
56+
),
57+
ChangeType.FIXED.name to listOf(
58+
"EHLO parsing exception appeared in logcat when sending email",
59+
"\"Delete (from notification)\" setting did not retain state",
60+
"Find folder search hint text was cut off on smaller screens",
61+
"User was not notified if they were offline when sending message",
62+
"Warned \"Sent folder not found\" when uploading sent messages was disabled",
63+
"Loading bar appeared detached from header, leaving visible gap",
64+
"CC/BCC fields were not expanded by default for Reply All",
65+
),
66+
),
67+
),
68+
69+
ReleaseUiModel(
70+
version = "17.0b4",
71+
date = "2026-02-24",
72+
isLatest = false,
73+
changes = mapOf(
74+
ChangeType.FIXED.name to listOf(
75+
"Application navigation and account switching could crash",
76+
),
77+
),
78+
),
79+
80+
ReleaseUiModel(
81+
version = "17.0b3",
82+
date = "2026-02-17",
83+
isLatest = false,
84+
changes = mapOf(
85+
ChangeType.FIXED.name to listOf(
86+
"Application could crash when rendering message list",
87+
"Avatar image updates did not propagate immediately",
88+
),
89+
),
90+
),
91+
92+
ReleaseUiModel(
93+
version = "17.0b2",
94+
date = "2026-02-04",
95+
isLatest = false,
96+
changes = mapOf(
97+
ChangeType.CHANGED.name to listOf(
98+
"Crash occurred in 17.0b1 split view mode while in landscape",
99+
),
100+
),
101+
),
102+
103+
ReleaseUiModel(
104+
version = "17.0b1",
105+
date = "2026-01-05",
106+
isLatest = false,
107+
changes = mapOf(
108+
ChangeType.CHANGED.name to listOf(
109+
"Account avatar customization",
110+
"Foldable device support with split-screen",
111+
"Email messages can be printed",
112+
"Unified folder account indicator identifies message ownership",
113+
"Display account avatar in message notifications",
114+
"Improved email rendering enabled enabled for beta",
115+
"Deleted/read messages in unified inbox may not update until manual refresh",
116+
"Edge-to-edge regressions affected account setup screens",
117+
"Tap behavior was unreliable in recipient fields",
118+
"Incorrect icon was displayed for 'Find folder'",
119+
"Device orientiation change duplicated recipient field text",
120+
"Copied link text incorrectly included CSS",
121+
"Unselecting 'Colorize contact pictures' setting did not persist",
122+
"Some general settings were not preserved after export and import",
123+
),
124+
),
125+
),
126+
127+
ReleaseUiModel(
128+
version = "16.0b4",
129+
date = "2026-01-04",
130+
isLatest = false,
131+
changes = mapOf(
132+
ChangeType.CHANGED.name to listOf(
133+
"OAuth SMTP authentication failed with some Microsoft servers",
134+
),
135+
),
136+
),
137+
)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.fsck.k9.ui.changelog
2+
3+
import androidx.compose.runtime.Stable
4+
import kotlinx.collections.immutable.ImmutableList
5+
import net.thunderbird.core.ui.contract.mvi.UnidirectionalViewModel
6+
7+
interface ChangelogContract {
8+
9+
interface ViewModel : UnidirectionalViewModel<State, Event, Effect>
10+
11+
@Stable
12+
data class State(
13+
val releaseItems: ImmutableList<ReleaseUiModel>,
14+
val showRecentChanges: Boolean,
15+
)
16+
17+
sealed interface Event {
18+
data class OnShowRecentChangesCheck(val isChecked: Boolean) : Event
19+
}
20+
21+
sealed interface Effect
22+
}

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangelogFragment.kt

Lines changed: 23 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@ import android.os.Bundle
44
import android.view.LayoutInflater
55
import android.view.View
66
import android.view.ViewGroup
7+
import androidx.compose.ui.platform.ComposeView
8+
import androidx.compose.ui.platform.ViewCompositionStrategy
79
import androidx.fragment.app.Fragment
8-
import androidx.recyclerview.widget.RecyclerView
9-
import androidx.recyclerview.widget.RecyclerView.ViewHolder
1010
import app.k9mail.core.android.common.compat.BundleCompat
11-
import com.fsck.k9.ui.R
12-
import com.fsck.k9.ui.base.loader.observeLoading
13-
import com.google.android.material.checkbox.MaterialCheckBox
14-
import com.google.android.material.textview.MaterialTextView
15-
import de.cketti.changelog.ReleaseItem
11+
import kotlin.getValue
12+
import net.thunderbird.core.ui.contract.mvi.observe
13+
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
14+
import org.koin.android.ext.android.inject
1615
import org.koin.androidx.viewmodel.ext.android.viewModel
1716
import org.koin.core.parameter.parametersOf
1817

1918
/**
2019
* Displays the changelog entries in a scrolling list
2120
*/
2221
class ChangelogFragment : Fragment() {
22+
private val themeProvider: FeatureThemeProvider by inject()
2323
private val viewModel: ChangelogViewModel by viewModel {
2424
val mode = arguments?.let {
2525
BundleCompat.getSerializable(it, ARG_MODE, ChangeLogMode::class.java)
@@ -28,90 +28,27 @@ class ChangelogFragment : Fragment() {
2828
}
2929

3030
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
31-
return inflater.inflate(R.layout.fragment_changelog, container, false)
32-
}
33-
34-
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
35-
val listView = view.findViewById<RecyclerView>(R.id.changelog_list)
36-
37-
viewModel.changelogState.observeLoading(
38-
owner = viewLifecycleOwner,
39-
loadingView = view.findViewById(R.id.changelog_loading),
40-
errorView = view.findViewById(R.id.changelog_error),
41-
dataView = listView,
42-
) { changeLog ->
43-
listView.adapter = ChangelogAdapter(changeLog)
44-
}
45-
46-
setUpShowRecentChangesCheckbox(view)
47-
}
48-
49-
private fun setUpShowRecentChangesCheckbox(view: View) {
50-
val showRecentChangesCheckBox = view.findViewById<MaterialCheckBox>(R.id.show_recent_changes_checkbox)
51-
var isInitialValue = true
52-
viewModel.showRecentChangesState.observe(viewLifecycleOwner) { showRecentChanges ->
53-
showRecentChangesCheckBox.isChecked = showRecentChanges
54-
if (isInitialValue) {
55-
// Don't animate when setting initial value
56-
showRecentChangesCheckBox.jumpDrawablesToCurrentState()
57-
isInitialValue = false
31+
return ComposeView(requireContext()).apply {
32+
setViewCompositionStrategy(
33+
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed,
34+
)
35+
36+
setContent {
37+
val (state, dispatch) = viewModel.observe {}
38+
themeProvider.WithTheme {
39+
ChangelogScreen(
40+
releaseItems = state.value.releaseItems,
41+
showRecentChanges = state.value.showRecentChanges,
42+
onShowRecentChangesCheck = {
43+
dispatch(ChangelogContract.Event.OnShowRecentChangesCheck(state.value.showRecentChanges))
44+
},
45+
)
46+
}
5847
}
5948
}
60-
showRecentChangesCheckBox.setOnCheckedChangeListener { _, isChecked ->
61-
viewModel.setShowRecentChanges(isChecked)
62-
}
6349
}
6450

6551
companion object {
6652
const val ARG_MODE = "mode"
6753
}
6854
}
69-
70-
class ChangelogAdapter(releaseItems: List<ReleaseItem>) : RecyclerView.Adapter<ViewHolder>() {
71-
private val items = releaseItems.flatMap { listOf(it) + it.changes }
72-
73-
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
74-
val layoutInflater = LayoutInflater.from(parent.context)
75-
val view = layoutInflater.inflate(viewType, parent, false)
76-
return when (viewType) {
77-
R.layout.changelog_list_release_item -> ReleaseItemViewHolder(view)
78-
R.layout.changelog_list_change_item -> ChangeItemViewHolder(view)
79-
else -> error("Unsupported view type: $viewType")
80-
}
81-
}
82-
83-
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
84-
when (val item = items[position]) {
85-
is ReleaseItem -> {
86-
val viewHolder = holder as ReleaseItemViewHolder
87-
val context = viewHolder.versionName.context
88-
viewHolder.versionName.text = context.getString(R.string.changelog_version_title, item.versionName)
89-
viewHolder.versionDate.text = item.date
90-
}
91-
92-
is String -> {
93-
val viewHolder = holder as ChangeItemViewHolder
94-
viewHolder.changeText.text = item
95-
}
96-
}
97-
}
98-
99-
override fun getItemViewType(position: Int): Int {
100-
return when (items[position]) {
101-
is ReleaseItem -> R.layout.changelog_list_release_item
102-
is String -> R.layout.changelog_list_change_item
103-
else -> error("Unsupported item type: ${items[position]}")
104-
}
105-
}
106-
107-
override fun getItemCount(): Int = items.size
108-
}
109-
110-
class ReleaseItemViewHolder(view: View) : ViewHolder(view) {
111-
val versionName: MaterialTextView = view.findViewById(R.id.version_name)
112-
val versionDate: MaterialTextView = view.findViewById(R.id.version_date)
113-
}
114-
115-
class ChangeItemViewHolder(view: View) : ViewHolder(view) {
116-
val changeText: MaterialTextView = view.findViewById(R.id.change_text)
117-
}

0 commit comments

Comments
 (0)