diff --git a/app/src/main/assets/changelogs/6000.md b/app/src/main/assets/changelogs/6000.md
new file mode 100644
index 00000000..c2faeadf
--- /dev/null
+++ b/app/src/main/assets/changelogs/6000.md
@@ -0,0 +1,90 @@
+# 
+
+## Welcome Beta Ring II 🎉
+
+For many of you, this is the first time you're seeing this app. Welcome to the second beta ring!
+You'll be helping us test the app and find bugs before we release it to the general public. Everyone
+is so excited to have you here!
+
+Crashes are expected, and we're working hard to fix them. If you find a crash, it will be reported
+automatically. If you have anything else to say, use the **channels on Jenvolt** or the **Feedback**
+button in settings.
+
+## Server Identities are here
+
+Server identities and role colours will now be shown across the app. This should help make the app
+feel a lot more like the web client, which is our compatibility target. Servers look a lot closer
+now!
+
+We'll still exploring how to best show these identities in user sheets, but for now, they're only
+shown inline in chat and in the member list.
+
+## Changelogs
+
+You're reading one right now! New releases will now come with changelogs, so you can see what's
+been cooking in the kitchen. We'll also be posting these on the website after general availability,
+so feel free to check them out there too.
+
+## Updated to Android 14 SDK
+
+We've updated the app to use the latest Android SDK, which is Android 14. This means we're now
+ahead of the actual Android release schedule! 14 is still in beta, but the SDK is stable by now.
+
+This will help us keep the
+app [in the Play Store](https://support.google.com/googleplay/android-developer/answer/11926878?hl=en)
+for longer, and it also means we can use the latest and greatest APIs. As always, the app's aim is
+to make use of the Android platform as much as possible, and being on the newest SDK helps us do
+that.
+
+## Extended Markdown in Bios and Changelogs
+
+There is now a separate **web-based** Markdown renderer, currently in use on user bios and
+changelogs.
+This renderer is more powerful than the one we use in chat, and it's also more accurate to the web
+client. It is a little slower, but we're working on that. The aim will be to instantly render
+Markdown in it, however the current placeholder implementation works alright. Look forward to KaTeX
+and more!
+
+Now, our focus will go back to the native chat Markdown renderer. It is quite a bit behind in terms
+of feature support. Some features (such as KaTeX) will be impossible to implement natively, so we
+will be looking at ways to fuse the two renderers together on-demand. As always, this is a long-term
+goal, but we're getting there.
+
+## Member List
+
+Here it is, the member list sheet! This is equivalent to the right sidebar on the web client. It
+shows all the members of the server, sorted by role and position, along with the correct role colour
+and identity.
+
+In the future, you will be able to filter this list by role, and search for members. For now, it's
+just a list.
+
+## The Small Things
+
+- Latest and greatest dependency versions are now used, including Kotlin 1.9.10 and Compose 1.5.3.
+- Audio player gained a "share URL" menu item.
+- Message timestamps now use native time formatting APIs, which means they'll be formatted
+ correctly for your locale and will stay accurate in all cases.
+- The disconnected/reconnected/connected banner is now using Material You colouring if your theme is
+ set to that.
+- Roles in the user sheet are now sorted by position.
+- If a users' profile is empty or fails to load, the fallback messages are now clearly
+ distinguishable as such.
+- Debug builds now have "+debug" appended to their version string, an app ID of "chat.revolt.debug",
+ and a different name. This allows you to install the debug build alongside the release build.
+
+## Squished Bug Showcase
+
+- Fixed a crash when opening the user sheet for a user that has blocked you.
+- Fixed a condition in which messages from yesterday would be shown as "Today" in the chat.
+- Fixed a condition in which GIFs would not play in the chat.
+- Fixed a condition in which animated WebP images would not play in the chat.
+- Fixed a condition in which users were eagerly shown as offline when they were actually online.
+- Fixed a condition in which the ripple area for server icons would be too small compared to the
+ icon.
+
+## 🫡✨
+
+That's all for now! We hope you enjoy this release, and we're looking forward to your feedback.
+Please report any bugs you find, and let us know what you think of the app so far. Thank you for
+testing!
\ No newline at end of file
diff --git a/app/src/main/assets/changelogs/assets/6000/header.png b/app/src/main/assets/changelogs/assets/6000/header.png
new file mode 100644
index 00000000..6656c543
Binary files /dev/null and b/app/src/main/assets/changelogs/assets/6000/header.png differ
diff --git a/app/src/main/assets/changelogs/index.json b/app/src/main/assets/changelogs/index.json
new file mode 100644
index 00000000..3946607a
--- /dev/null
+++ b/app/src/main/assets/changelogs/index.json
@@ -0,0 +1,10 @@
+{
+ "list": {
+ "6000": {
+ "summary": "Beta Ring II, Server Identities, Changelogs, SDK34",
+ "version": "0.6.0",
+ "date": "2023-09-08"
+ }
+ },
+ "latest": "6000"
+}
\ No newline at end of file
diff --git a/app/src/main/assets/webmarkdown/renderer.html b/app/src/main/assets/webmarkdown/renderer.html
index 0ed13253..a99006a0 100644
--- a/app/src/main/assets/webmarkdown/renderer.html
+++ b/app/src/main/assets/webmarkdown/renderer.html
@@ -62,11 +62,8 @@
diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt
index b1d5a2d4..00d0a880 100644
--- a/app/src/main/java/chat/revolt/activities/MainActivity.kt
+++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt
@@ -39,6 +39,7 @@ import chat.revolt.screens.register.RegisterDetailsScreen
import chat.revolt.screens.register.RegisterGreetingScreen
import chat.revolt.screens.register.RegisterVerifyScreen
import chat.revolt.screens.settings.AppearanceSettingsScreen
+import chat.revolt.screens.settings.ChangelogsSettingsScreen
import chat.revolt.screens.settings.ClosedBetaUpdaterScreen
import chat.revolt.screens.settings.DebugSettingsScreen
import chat.revolt.screens.settings.SettingsScreen
@@ -137,6 +138,7 @@ fun AppEntrypoint(windowSizeClass: WindowSizeClass) {
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
composable("settings/debug") { DebugSettingsScreen(navController) }
composable("settings/updater") { ClosedBetaUpdaterScreen(navController) }
+ composable("settings/changelogs") { ChangelogsSettingsScreen(navController) }
dialog("settings/feedback") { FeedbackDialog(navController) }
composable("about") { AboutScreen(navController) }
diff --git a/app/src/main/java/chat/revolt/components/generic/WebMarkdown.kt b/app/src/main/java/chat/revolt/components/generic/WebMarkdown.kt
index d31e1762..76193610 100644
--- a/app/src/main/java/chat/revolt/components/generic/WebMarkdown.kt
+++ b/app/src/main/java/chat/revolt/components/generic/WebMarkdown.kt
@@ -49,6 +49,7 @@ private fun argbAsCssColour(argb: Int): String {
fun WebMarkdown(
text: String,
maskLoading: Boolean = false,
+ simpleLineBreaks: Boolean = true,
modifier: Modifier = Modifier,
) {
val contentColour = LocalContentColor.current
@@ -128,7 +129,7 @@ fun WebMarkdown(
}
loadUrl(
- "https://app.revolt.chat/_android_assets/webmarkdown/renderer.html",
+ "$REVOLT_APP/_android_assets/webmarkdown/renderer.html",
)
settings.apply {
@@ -164,6 +165,11 @@ fun WebMarkdown(
fun getPrimaryColour(): String {
return argbAsCssColour(materialColourScheme.primary.toArgb())
}
+
+ @JavascriptInterface
+ fun shouldUseSimpleLineBreaks(): Boolean {
+ return simpleLineBreaks
+ }
},
"Bridge"
)
@@ -174,6 +180,9 @@ fun WebMarkdown(
LayoutParams.WRAP_CONTENT
)
}
+ },
+ update = {
+ it.evaluateJavascript("renderMarkdown()", null)
}
)
}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/internals/Changelogs.kt b/app/src/main/java/chat/revolt/internals/Changelogs.kt
new file mode 100644
index 00000000..ef97b306
--- /dev/null
+++ b/app/src/main/java/chat/revolt/internals/Changelogs.kt
@@ -0,0 +1,45 @@
+package chat.revolt.internals
+
+import android.content.Context
+import chat.revolt.api.RevoltJson
+import chat.revolt.persistence.KVStorage
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Changelog(
+ val summary: String,
+ val version: String,
+ val date: String,
+)
+
+@Serializable
+data class ChangelogIndex(
+ val list: Map,
+ val latest: String
+)
+
+class Changelogs(val context: Context, val kvStorage: KVStorage? = null) {
+ val index = context.assets.open("changelogs/index.json").use {
+ it.reader().readText()
+ }.let {
+ RevoltJson.decodeFromString(ChangelogIndex.serializer(), it)
+ }
+
+ fun getChangelog(version: String): String {
+ return context.assets.open("changelogs/${version}.md").use {
+ it.reader().readText()
+ }
+ }
+
+ suspend fun hasSeenLatest(): Boolean {
+ if (kvStorage == null) throw IllegalStateException("Not supported for non-KVStorage instances of Changelogs")
+
+ return kvStorage.get("latestChangelogRead") == index.latest
+ }
+
+ suspend fun markAsSeen() {
+ if (kvStorage == null) throw IllegalStateException("Not supported for non-KVStorage instances of Changelogs")
+
+ kvStorage.set("latestChangelogRead", index.latest)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
index 805c6d37..30ececc5 100644
--- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt
@@ -1,5 +1,7 @@
package chat.revolt.screens.chat
+import android.annotation.SuppressLint
+import android.content.Context
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
@@ -73,12 +75,14 @@ import chat.revolt.components.screens.chat.drawer.channel.ChannelList
import chat.revolt.components.screens.chat.drawer.server.DrawerServer
import chat.revolt.components.screens.chat.drawer.server.DrawerServerlikeIcon
import chat.revolt.components.screens.chat.drawer.server.ServerDrawerSeparator
+import chat.revolt.internals.Changelogs
import chat.revolt.persistence.KVStorage
import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog
import chat.revolt.screens.chat.views.HomeScreen
import chat.revolt.screens.chat.views.NoCurrentChannelScreen
import chat.revolt.screens.chat.views.channel.ChannelScreen
import chat.revolt.sheets.AddServerSheet
+import chat.revolt.sheets.ChangelogSheet
import chat.revolt.sheets.ServerContextSheet
import chat.revolt.sheets.StatusSheet
import chat.revolt.sheets.UserContextSheet
@@ -88,27 +92,41 @@ import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
+@SuppressLint("StaticFieldLeak")
class ChatRouterViewModel @Inject constructor(
- private val kvStorage: KVStorage
+ private val kvStorage: KVStorage,
+ @ApplicationContext val context: Context
) : ViewModel() {
var currentServer by mutableStateOf("home")
var currentChannel by mutableStateOf(null)
var sidebarSparkDisplayed by mutableStateOf(true)
+ var latestChangelogRead by mutableStateOf(true)
+ var latestChangelog by mutableStateOf("")
+
+ private val changelogs = Changelogs(context, kvStorage)
init {
viewModelScope.launch {
currentServer = kvStorage.get("currentServer") ?: "home"
currentChannel = kvStorage.get("currentChannel")
+
sidebarSparkDisplayed = if (kvStorage.getBoolean("sidebarSpark") == null) {
false
} else {
kvStorage.getBoolean("sidebarSpark")!!
}
+
+ latestChangelogRead = changelogs.hasSeenLatest()
+ latestChangelog = changelogs.index.latest
+ if (!latestChangelogRead) {
+ changelogs.markAsSeen()
+ }
}
}
@@ -291,6 +309,22 @@ fun ChatRouterScreen(
}
}
+ if (!viewModel.latestChangelogRead) {
+ val changelogSheetState = rememberModalBottomSheetState()
+
+ ModalBottomSheet(
+ sheetState = changelogSheetState,
+ onDismissRequest = {
+ viewModel.latestChangelogRead = true
+ },
+ ) {
+ ChangelogSheet(
+ version = viewModel.latestChangelog,
+ new = true
+ )
+ }
+ }
+
if (showSidebarSpark.value) {
AlertDialog(
onDismissRequest = {},
diff --git a/app/src/main/java/chat/revolt/screens/settings/ChangelogsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/ChangelogsScreen.kt
new file mode 100644
index 00000000..b9797185
--- /dev/null
+++ b/app/src/main/java/chat/revolt/screens/settings/ChangelogsScreen.kt
@@ -0,0 +1,115 @@
+package chat.revolt.screens.settings
+
+import android.content.Context
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawingPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.ViewModel
+import androidx.navigation.NavController
+import chat.revolt.R
+import chat.revolt.components.generic.PageHeader
+import chat.revolt.internals.Changelogs
+import chat.revolt.persistence.KVStorage
+import chat.revolt.sheets.ChangelogSheet
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+
+@HiltViewModel
+class ChangelogsSettingsScreenViewModel @Inject constructor(
+ kvStorage: KVStorage,
+ @ApplicationContext context: Context
+) : ViewModel() {
+ val index = Changelogs(context, kvStorage).index
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ChangelogsSettingsScreen(
+ navController: NavController,
+ viewModel: ChangelogsSettingsScreenViewModel = hiltViewModel()
+) {
+ var currentChangelog by remember { mutableStateOf(viewModel.index.latest) }
+ var sheetOpen by remember { mutableStateOf(false) }
+
+ if (sheetOpen) {
+ val sheetState = rememberModalBottomSheetState()
+
+ ModalBottomSheet(
+ sheetState = sheetState,
+ onDismissRequest = {
+ sheetOpen = false
+ }
+ ) {
+ ChangelogSheet(version = currentChangelog)
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .safeDrawingPadding()
+ ) {
+ PageHeader(
+ text = stringResource(R.string.settings_changelogs),
+ showBackButton = true,
+ onBackButtonClicked = {
+ navController.popBackStack()
+ })
+
+ LazyColumn {
+ items(
+ viewModel.index.list.size,
+ key = { viewModel.index.list.keys.elementAt(it) }
+ ) { index ->
+ val version = viewModel.index.list.keys.elementAt(index)
+ val changelog = viewModel.index.list[version]!!
+
+ Column(
+ modifier = Modifier
+ .clickable {
+ currentChangelog = version
+ sheetOpen = true
+ }
+ .fillMaxWidth()
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ Text(
+ text = changelog.version,
+ style = MaterialTheme.typography.headlineSmall
+ )
+ Text(
+ text = changelog.summary,
+ style = MaterialTheme.typography.bodyMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ Divider()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/screens/settings/DebugSettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/DebugSettingsScreen.kt
index 7c722f06..e2c5e27b 100644
--- a/app/src/main/java/chat/revolt/screens/settings/DebugSettingsScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/settings/DebugSettingsScreen.kt
@@ -38,6 +38,12 @@ class DebugSettingsScreenViewModel @Inject constructor(
fun forgetAllSparks() {
this.forgetSidebarSparkShown()
}
+
+ fun forgetLatestChangelog() {
+ viewModelScope.launch {
+ kvStorage.remove("latestChangelogRead")
+ }
+ }
}
@Composable
@@ -79,6 +85,19 @@ fun DebugSettingsScreen(
Text("Forget all sparks")
}
}
+
+ Text(
+ text = "Changelogs",
+ style = MaterialTheme.typography.headlineSmall,
+ modifier = Modifier.padding(bottom = 10.dp)
+ )
+ Row(
+ modifier = Modifier.horizontalScroll(rememberScrollState())
+ ) {
+ ElevatedButton(onClick = { viewModel.forgetLatestChangelog() }) {
+ Text("Mark latest changelog as unread")
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt
index 9f69511e..05f8beeb 100644
--- a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt
@@ -12,6 +12,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Star
@@ -154,6 +155,25 @@ fun SettingsScreen(
modifier = Modifier.padding(bottom = 10.dp, start = 10.dp, top = 20.dp)
)
+ SheetClickable(
+ icon = { modifier ->
+ Icon(
+ imageVector = Icons.Default.DateRange,
+ contentDescription = stringResource(id = R.string.settings_changelogs),
+ modifier = modifier
+ )
+ },
+ label = { textStyle ->
+ Text(
+ text = stringResource(id = R.string.settings_changelogs),
+ style = textStyle
+ )
+ },
+ modifier = Modifier.testTag("settings_view_changelogs")
+ ) {
+ navController.navigate("settings/changelogs")
+ }
+
SheetClickable(
icon = { modifier ->
Icon(
diff --git a/app/src/main/java/chat/revolt/sheets/ChangelogSheet.kt b/app/src/main/java/chat/revolt/sheets/ChangelogSheet.kt
new file mode 100644
index 00000000..5dc75aac
--- /dev/null
+++ b/app/src/main/java/chat/revolt/sheets/ChangelogSheet.kt
@@ -0,0 +1,106 @@
+package chat.revolt.sheets
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.ViewModel
+import chat.revolt.R
+import chat.revolt.components.generic.PageHeader
+import chat.revolt.components.generic.WebMarkdown
+import chat.revolt.internals.Changelog
+import chat.revolt.internals.Changelogs
+import chat.revolt.persistence.KVStorage
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+
+@HiltViewModel
+@SuppressLint("StaticFieldLeak")
+class ChangelogSheetViewModel @Inject constructor(
+ val kvStorage: KVStorage,
+ @ApplicationContext val context: Context
+) : ViewModel() {
+ private val changelogs = Changelogs(context, kvStorage)
+ var changelogContents by mutableStateOf(null as String?)
+ var changelog by mutableStateOf(null as Changelog?)
+
+ private fun getContents(version: String): String {
+ return changelogs.getChangelog(version)
+ }
+
+ private fun getChangelog(version: String): Changelog {
+ return changelogs.index.list[version] ?: throw IllegalStateException("Changelog not found")
+ }
+
+ fun populate(version: String) {
+ changelogContents = getContents(version)
+ changelog = getChangelog(version)
+ }
+}
+
+@Composable
+fun ChangelogSheet(
+ version: String,
+ new: Boolean = false,
+ viewModel: ChangelogSheetViewModel = hiltViewModel()
+) {
+ LaunchedEffect(version) {
+ viewModel.populate(version)
+ }
+
+ Column {
+ PageHeader(
+ if (new) {
+ stringResource(R.string.settings_changelogs_new_header)
+ } else {
+ stringResource(
+ R.string.settings_changelogs_historical_version_header,
+ viewModel.changelog?.version
+ ?: stringResource(R.string.settings_changelogs_historical_version_header_placeholder)
+ )
+ }
+ )
+
+ if (viewModel.changelogContents == null) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ ) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+ }
+
+ key(viewModel.changelogContents) {
+ Column(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+ WebMarkdown(
+ text = viewModel.changelogContents ?: "",
+ maskLoading = true,
+ simpleLineBreaks = false
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ae0a1e20..6b8770f3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -359,4 +359,9 @@
Feedback unavailable
Feedback is not available on this build of Revolt. Support for this build is limited. (Build: %1$s %2$s)
+
+ Changelogs
+ What\'s been cooking ✨
+ Changelog for %1$s
+ that version