feat: changelog system

Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
Infi 2023-09-08 03:45:40 +02:00
parent 3a5098ecf1
commit e625297718
13 changed files with 469 additions and 9 deletions

View File

@ -0,0 +1,90 @@
# ![Revolt for Android 0.6.0](/_android_assets/changelogs/assets/6000/header.png)
## 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!

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,10 @@
{
"list": {
"6000": {
"summary": "Beta Ring II, Server Identities, Changelogs, SDK34",
"version": "0.6.0",
"date": "2023-09-08"
}
},
"latest": "6000"
}

View File

@ -62,11 +62,8 @@
<script src="https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"></script>
<script>
window.addEventListener("load", () => {
const style = document.querySelector("style")
style.innerHTML = style.innerHTML
.replaceAll("{content}", Bridge.getContentColour())
.replaceAll("{primary}", Bridge.getPrimaryColour())
const renderMarkdown = () => {
const markdown = document.querySelector("#markdown")
const converter = new showdown.Converter()
@ -75,14 +72,22 @@
converter.setOption("emoji", true)
converter.setOption("disableForced4SpacesIndentedSublists", true)
converter.setOption("noHeaderId", true)
converter.setOption("simpleLineBreaks", true)
converter.setOption("simpleLineBreaks", Bridge.shouldUseSimpleLineBreaks())
converter.setOption("strikethrough", true)
converter.setOption("tasklists", true)
const markdown = document.querySelector("#markdown")
const html = converter.makeHtml(Bridge.getMarkdown())
markdown.innerHTML = DOMPurify.sanitize(html)
}
window.renderMarkdown = renderMarkdown
window.addEventListener("load", () => {
const style = document.querySelector("style")
style.innerHTML = style.innerHTML
.replaceAll("{content}", Bridge.getContentColour())
.replaceAll("{primary}", Bridge.getPrimaryColour())
renderMarkdown()
Bridge.onLoaded()
})
</script>

View File

@ -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) }

View File

@ -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)
}
)
}

View File

@ -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<String, Changelog>,
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)
}
}

View File

@ -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<String?>(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 = {},

View File

@ -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()
}
}
}
}
}

View File

@ -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")
}
}
}
}
}

View File

@ -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(

View File

@ -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
)
}
}
}
}

View File

@ -359,4 +359,9 @@
<string name="settings_feedback_disabled_title">Feedback unavailable</string>
<string name="settings_feedback_disabled_message">Feedback is not available on this build of Revolt. Support for this build is limited. (Build: %1$s %2$s)</string>
<string name="settings_changelogs">Changelogs</string>
<string name="settings_changelogs_new_header">What\'s been cooking ✨</string>
<string name="settings_changelogs_historical_version_header">Changelog for %1$s</string>
<string name="settings_changelogs_historical_version_header_placeholder">that version</string>
</resources>