feat: use new changelogs system

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-06-22 17:44:58 +02:00
parent 278d8ac5de
commit 9bdada6fab
15 changed files with 358 additions and 560 deletions

View File

@ -1,90 +0,0 @@
# ![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!

View File

@ -1,92 +0,0 @@
# ![Revolt on Android 0.6.1](/_android_assets/changelogs/assets/6001/header.png)
Hello Revolters! Onwards to another release.
## Links
This is the big one. Links are here.
You can now tap on links in messages and they will open in your browser.
Never seen before.
Jokes aside, this is obviously very important for usability.
![Demo of links.](https://autumn.revolt.chat/attachments/9i6x3L48Z4xzbZwUg9q-Z2NqBdk1UupMsr-3Vi7XaP)
As you can see, it works for channel and user mentions as well! They even have their own special
little highlight.
If you long tap a link, you will be presented with an action sheet. This action sheet allows you to
either open the link in your browser, or copy the link to your clipboard.
![Demo of link sheet.](https://autumn.revolt.chat/attachments/j2pMxFeShhfWgoFP8IcAUieR2ShpUbIQEkVirM3YYM/image.png)
Getting this to work has been more difficult than it should have been. The main issue was that
the TextView API in Android should be considered a war crime. One custom touch handler later, and
we have a working solution.
## Custom Emotes in Messages
Custom emotes are now displayed inline in messages. This means that you can now see the emotes
that someone sends.
![Demo of custom emotes](https://autumn.revolt.chat/attachments/d5W4rAGsDOzJPKVWfcz5-icxzK0rOfBYN0SevVfPCp/image.png)
As one would expect, when a message consists solely of emotes, they will be displayed larger.
![Demo of large emotes](https://autumn.revolt.chat/attachments/k4iw9OtAVQz5CpkuOVY_R6CpIqHgexqfiHj6NfrkJl/image.png)
## Permissions
This release adds support for channel, server and role permissions. This means that if you don't
have permission to, for instance,
send messages in a channel, you will be notified of that.
![Demo of permissions](https://autumn.revolt.chat/attachments/F5jE_Um6I_4qLFwKljKpjvvAztiq7_56dsF9sFBG4T/image.png)
This isn't a huge change, but it required some pretty heavy lifting on the development side.
## New Home Screen
![New home screen demo](https://autumn.revolt.chat/attachments/54AVadXT9dZUTmvjB6u3D_B1GsWFEy_EsplCjsvupL/image.png)
There's a cat. 'Nuff said.
## Server Badges
Servers sometimes get verified by Revolt, for example when they're noteworthy communities or when
they're official communities. This release adds support for displaying these badges.
![Official badge demo](https://autumn.revolt.chat/attachments/aK6uaug9pevx2WEdxeLuo-D8TVMGwb6RikfOAKDYyf/image.png)
Of course, servers ran by Revolt also get their own special badge that you can see above. Both of
those badges are also
displayed in the server context sheet.
![Verified badge demo](https://autumn.revolt.chat/attachments/EaeTmWUTzkW5b13toaShu4pA2YoXkQ2HcAJF2gVSCC/image.png)
## The Small Things
As always, some additions have not made their own heading. Don't worry, here they are:
- Do not hardcode the attachment authority of the application. If the package name differs, the app
will not crash when providing an attachment (for example, by sharing a file).
- `WebCompat` will no longer spam Logcat when resolving colours correctly.
- Session token is no longer fetched in every route implementation. This helps make the codebase a
little cleaner.
- Access control ("DRM"—in quotes) is now a native C++ module.
- Member list now has support for group chats.
- Opening the member list in group chats, direct messages or saved messages no longer crashes the
app.
- Timestamps now have the correct monospaced font.
- If you cannot send messages in a channel, that information will now be displayed in a
nicer-looking way than before.
- Additional safeguard for sending the same message multiple times by mashing the send button.
- Jenvolt link removed from the settings screen.
- Upgrade the Android Gradle Plugin, twice.
- Empty channels will no longer show as having unread messages.
- Editing a message now correctly causes your text cursor to start at the end of the message.
## Wrapping Up
That's it for this release. If you have any feedback, please let the team know using the usual
channels. We're always happy to hear from you.

View File

@ -1,86 +0,0 @@
# ![Revolt on Android 0.7.0](https://autumn.revolt.chat/attachments/1VSp9d6ZZQEYFNtnM0E_MtmaUMQAPIMaLpEvYEKSt8/Slide%2016_9%20-%201%20(7).png)
Hello Revolters! Continuing our roughly monthly release schedule, this is a big one.
## ![Themes](https://autumn.revolt.chat/attachments/OxkrZPo-eJpuXjJ6ZWD-uQrvK9fxhsiEXWSs2MG7fw/Slide%2016_9%20-%202%20(3).png)
"...but Themes were already in the app!", I hear you say. "What's new?", you ask.
Besides a polished, beautiful, and more concise user interface, there's one important addition. You
can now create your own themes and share them with others! Customise every colourful aspect of the
Material 3 design language, and export them to an ultra-compact `.RATO` (Revolt Android Theme
Overrides, but I've been told it means "rat" in Portuguese) file.
Be sure to share your creations in [Jenvolt](https://rvlt.gg/jen)'s #themes channel. I'm sure some
of them will even be uploaded to an official theme repository in the future 👀
## ![Settings](https://autumn.revolt.chat/attachments/1GoQIIc1NUQI3EkAOjVmxT9MIsFPIDTNwBa98PBpB7/Slide%2016_9%20-%203.png)
Wake up, new settings dropped. You can now change your profile picture, profile background and bio
right from the Android app, because it is *essential* that everyone knows you're a furry. I mean,
that you're a gamer. I mean, that you're a furry gamer. I'm sure you get the point.
Of course, around here we also like to emphasise security, so that's why you can now manage your
active devices and sessions right from the app. If you see a device or session you don't recognise,
you can terminate it on the go. If you're paranoid, you can also terminate all other sessions at
once — I'm sure this will be useful for some of you.
## ![Emoji Picker](https://autumn.revolt.chat/attachments/tXrdK8EZRCSM9gjtsycwrTldQG9205wxKkU4hx6cXJ/Slide%2016_9%20-%204.png)
What's that next to the send button? Could it be... an emoji picker? Yes, yes it is!
Tap once to see all your servers lined up neatly above the performant grid of emojis. That's the
custom emotes of the servers you're in, of course. Scroll in the server list, all the way, to see
the standard Unicode emoji. Tap on an emoji to add it to your message and spread the joy. There's
even a search bar, if you're into that. I've seen the amount of servers some of you are in —
so some of you are definitely into that.
## ![Friends](https://autumn.revolt.chat/attachments/SJ1KCW9QgiYfCN5z7YJUeYzzKicmNya20BLtpVpay5/Slide%2016_9%20-%205.png)
You can now view your incoming friend requests from the app. I'm sure you get lots of them.
Friends are not included and are sold separately.
## ![Status](https://autumn.revolt.chat/attachments/Oz7WdIr0Cu5M2G5Sah51rTlH4la_MD37R5HeX72TPA/Slide%2016_9%20-%206.png)
Look at this status UI. It is simply beautiful — and it's so simple to use, too! You can now
set your status right from the app, and get a quick explainer blurb on what each status means. I
mean, what the hell is
a ["Focus"](https://upload.wikimedia.org/wikipedia/commons/b/b2/Ford_Focus_2004.jpg) anyway?
## ![Channel Categories](https://autumn.revolt.chat/attachments/D1iG_sdhhSrHHs_QS6cZDB0zNif57df2tg3Mdratty/Slide%2016_9%20-%207.png)
Lots of people have been asking for this one, and honestly, I've been too. But since I will not let
myself (and you Android folks) fall behind the iOS app, I've implemented it. Channels in the channel
list are now grouped by the category the server's owner has put them in, and are now in the correct
order, too. This is revolutionary.
## Other changes
- Do not show "Copied X to clipboard" toast on Android 12 and later, as the system already shows a
toast when you copy something.
- Channels should feel like they load faster now, because I am a master at psychology.
- The changelog description for 0.6.1 has been fixed, because I am not a master at copy-pasting.
- Some translations that have been sitting around for a while have been merged in. Apologies for the
wait. You might hear some more info on Android translations soon, stay tuned.
- Channel mentions now lead you to the correct channel, not the first channel in the server.
- Images load in with a nice fade-in animation now. I can't even describe the impact this has on me,
it's just so much better.
- If you try to follow an invalid user mention, the resulting error sheet looks a lot nicer than
before.
- On Android 14, the app will now detect if you only granted partial access to media, and will ask
you to reconsider. We're up to date!
- The message field has been rewritten, which allows users of certain custom keyboards to type
again.
- You can now drag images into the message field, as well as insert them from the clipboard and your
keyboard's GIF picker, for instance.
- Users are now no longer shown as online when actually offline.
- The "Home" page now has a menu button in its top bar which opens the sidebar.
- The "Home" page now has a home icon instead of a text channel icon.
- The indicator showing the current sidebar page now doesn't stay at its previous position when you
switch pages.
- Embeds no longer show up as empty if scrolled away and then back to.
- System messages now have clickable user mentions, and display the correct user name.
- Release builds no longer log to logcat. This is a security improvement.
- The heart in the about screen is now an emoji heart, because the iOS app has one and we don't want
to get jealous.
- Synced settings are now fetched when you are successfully authenticated, not when the app starts.
This makes settings a bit more reliable.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,20 +0,0 @@
{
"list": {
"6000": {
"summary": "Beta Ring II, Server Identities, Changelogs, SDK34",
"version": "0.6.0",
"date": "2023-09-08"
},
"6001": {
"summary": "Links, mentions, channels and emotes in messages, permissions",
"version": "0.6.1",
"date": "2023-10-03"
},
"7000": {
"summary": "Emoji picker, friends menu, categories and more",
"version": "0.7.0",
"date": "2023-10-31"
}
},
"latest": "7000"
}

View File

@ -12,6 +12,13 @@
font-style: normal;
}
@font-face {
font-family: "Inter Display";
src: url("/_android_res/font/inter_display_semibold.ttf");
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: "Inter";
src: url("/_android_res/font/inter_bold.ttf");
@ -55,29 +62,23 @@
max-width: 100%;
height: auto;
}
#markdown h2 {
font-size: 1.2em;
font-family: "Inter Display", sans-serif;
font-weight: 600;
}
#markdown p, #markdown li {
line-height: 1.5;
}
</style>
</head>
<body>
<div id="markdown"></div>
<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>
const renderMarkdown = () => {
const markdown = document.querySelector("#markdown")
const converter = new showdown.Converter()
converter.setFlavor("github")
converter.setOption("tables", true)
converter.setOption("emoji", true)
converter.setOption("disableForced4SpacesIndentedSublists", true)
converter.setOption("noHeaderId", true)
converter.setOption("simpleLineBreaks", Bridge.shouldUseSimpleLineBreaks())
converter.setOption("strikethrough", true)
converter.setOption("tasklists", true)
const html = converter.makeHtml(Bridge.getMarkdown())
markdown.innerHTML = DOMPurify.sanitize(html)
markdown.innerHTML = Bridge.getMarkdown()
}
window.renderMarkdown = renderMarkdown

View File

@ -55,6 +55,7 @@ const val REVOLT_JANUARY = "https://jan.revolt.chat"
const val REVOLT_APP = "https://app.revolt.chat"
const val REVOLT_INVITES = "https://rvlt.gg"
const val REVOLT_WEBSOCKET = "wss://ws.revolt.chat"
const val REVOLT_KJBOOK = "https://revoltchat.github.io/android"
fun buildUserAgent(accessMethod: String = "Ktor"): String {
return "$accessMethod RevoltAndroid/${BuildConfig.VERSION_NAME} ${BuildConfig.APPLICATION_ID} (Android ${android.os.Build.VERSION.SDK_INT}; ${android.os.Build.MANUFACTURER} ${android.os.Build.DEVICE}; (Kotlin ${KotlinVersion.CURRENT})"

View File

@ -3,9 +3,16 @@ package chat.revolt.api.internals
import android.content.Context
import android.content.ContextWrapper
import androidx.activity.ComponentActivity
import androidx.fragment.app.FragmentActivity
fun Context.getComponentActivity(): ComponentActivity? = when (this) {
is ComponentActivity -> this
is ContextWrapper -> baseContext.getComponentActivity()
else -> null
}
fun Context.getFragmentActivity(): FragmentActivity? = when (this) {
is FragmentActivity -> this
is ContextWrapper -> baseContext.getFragmentActivity()
else -> null
}

View File

@ -1,9 +1,13 @@
package chat.revolt.components.generic
package chat.revolt.fragments
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.view.ViewGroup.LayoutParams
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.JavascriptInterface
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
@ -11,65 +15,52 @@ import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.res.ResourcesCompat
import androidx.webkit.WebViewAssetLoader
import chat.revolt.R
import chat.revolt.activities.InviteActivity
import chat.revolt.api.REVOLT_APP
import chat.revolt.databinding.SheetChangelogBinding
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.color.MaterialColors
private fun argbAsCssColour(argb: Int): String {
val alpha = (argb shr 24 and 0xff) / 255.0f
val red = argb shr 16 and 0xff
val green = argb shr 8 and 0xff
val blue = argb and 0xff
return String.format("#%02x%02x%02x%02x", red, green, blue, (alpha * 255).toInt())
}
/**
* WebView-backed Markdown renderer that supports all Markdown features
* including KaTeX
*/
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun WebMarkdown(
text: String,
modifier: Modifier = Modifier,
maskLoading: Boolean = false,
simpleLineBreaks: Boolean = true,
) {
val contentColour = LocalContentColor.current
val materialColourScheme = MaterialTheme.colorScheme
class ChangelogBottomSheetFragment(
val onDismiss: () -> Unit
) : BottomSheetDialogFragment() {
private lateinit var binding: SheetChangelogBinding
var finishedLoading by remember { mutableStateOf(false) }
if (!finishedLoading && maskLoading) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
}
private fun argbAsCssColour(argb: Int): String {
val alpha = (argb shr 24 and 0xff) / 255.0f
val red = argb shr 16 and 0xff
val green = argb shr 8 and 0xff
val blue = argb and 0xff
return String.format("#%02x%02x%02x%02x", red, green, blue, (alpha * 255).toInt())
}
AndroidView(
modifier = modifier,
factory = { context ->
WebView(context).apply {
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = SheetChangelogBinding.inflate(inflater, container, false)
requireArguments().run {
binding.tvTitle.apply {
text = when {
getBoolean(ARG_HISTORICAL) -> requireContext().getString(
R.string.settings_changelogs_historical_version_header,
getString(ARG_VERSION_NAME)
)
else -> requireContext().getString(R.string.settings_changelogs_new_header)
}
typeface = ResourcesCompat.getFont(requireContext(), R.font.inter_display_semibold)
}
binding.wvChangelog.apply {
val assetLoader = WebViewAssetLoader.Builder()
.setDomain(Uri.parse(REVOLT_APP).host!!)
.addPathHandler(
@ -119,7 +110,12 @@ fun WebMarkdown(
.setShowTitle(true)
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(materialColourScheme.background.toArgb())
.setToolbarColor(
MaterialColors.getColor(
binding.wvChangelog,
com.google.android.material.R.attr.backgroundColor
)
)
.build()
)
.build()
@ -131,7 +127,7 @@ fun WebMarkdown(
}
loadUrl(
"$REVOLT_APP/_android_assets/webmarkdown/renderer.html"
"$REVOLT_APP/_android_assets/changelogs/renderer.html"
)
settings.apply {
@ -145,46 +141,62 @@ fun WebMarkdown(
addJavascriptInterface(
object {
@JavascriptInterface
fun onLoaded() {
finishedLoading = true
}
@JavascriptInterface
fun getMarkdown(): String {
return text
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
return getString(ARG_RENDERED_CONTENTS) ?: ""
}
@JavascriptInterface
fun getContentColour(): String {
return argbAsCssColour(contentColour.toArgb())
return argbAsCssColour(
MaterialColors.getColor(
binding.wvChangelog,
com.google.android.material.R.attr.colorOnSurface
)
)
}
@JavascriptInterface
fun getPrimaryColour(): String {
return argbAsCssColour(materialColourScheme.primary.toArgb())
}
@JavascriptInterface
fun shouldUseSimpleLineBreaks(): Boolean {
return simpleLineBreaks
return argbAsCssColour(
MaterialColors.getColor(
binding.wvChangelog,
com.google.android.material.R.attr.colorPrimary
)
)
}
},
"Bridge"
)
setBackgroundColor(android.graphics.Color.TRANSPARENT)
layoutParams = FrameLayout.LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT
)
}
},
update = {
it.evaluateJavascript("renderMarkdown()", null)
}
)
}
return binding.root
}
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
onDismiss()
}
companion object {
const val TAG = "ChangelogBottomSheetFragment"
private const val ARG_VERSION_NAME = "version_name"
private const val ARG_HISTORICAL = "historical"
private const val ARG_RENDERED_CONTENTS = "rendered_contents"
fun createArguments(
versionName: String,
historical: Boolean,
renderedContents: String,
): Bundle {
return Bundle().apply {
putString(ARG_VERSION_NAME, versionName)
putBoolean(ARG_HISTORICAL, historical)
putString(ARG_RENDERED_CONTENTS, renderedContents)
}
}
}
}

View File

@ -1,48 +1,98 @@
package chat.revolt.internals
import android.content.Context
import chat.revolt.BuildConfig
import chat.revolt.api.REVOLT_KJBOOK
import chat.revolt.api.RevoltHttp
import chat.revolt.api.RevoltJson
import chat.revolt.internals.IndexHolder.cachedIndex
import chat.revolt.persistence.KVStorage
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.Serializable
@Serializable
data class Changelog(
val summary: String,
val version: String,
val date: String
data class ChangelogIndex(
val changelogs: List<ChangelogData> = emptyList()
)
@Serializable
data class ChangelogIndex(
val list: Map<String, Changelog>,
val latest: String
data class ChangelogData(
val version: ChangelogVersion,
val date: ChangelogDate,
val summary: String
)
@Serializable
data class ChangelogDate(
val publish: String
)
@Serializable
data class ChangelogVersion(
val code: Long,
val name: String,
val title: String
)
@Serializable
data class Changelog(
val id: String,
val slug: String,
val body: String,
val collection: String,
val data: ChangelogData,
val rendered: String
)
object IndexHolder {
var cachedIndex: ChangelogIndex? = null
}
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 getList(): Map<String, Changelog> {
return index.list.entries.reversed().associate { it.key to it.value }
}
fun getChangelog(version: String): String {
return context.assets.open("changelogs/$version.md").use {
it.reader().readText()
suspend fun fetchChangelogIndex(): ChangelogIndex {
if (cachedIndex != null) {
return cachedIndex as ChangelogIndex
}
val response = RevoltHttp.get("$REVOLT_KJBOOK/changelogs.json")
cachedIndex =
RevoltJson.decodeFromString(ChangelogIndex.serializer(), response.bodyAsText())
return cachedIndex as ChangelogIndex
}
suspend fun hasSeenLatest(): Boolean {
suspend fun fetchChangelogByVersionCode(versionCode: Long): Changelog {
val response = RevoltHttp.get("$REVOLT_KJBOOK/changelogs/$versionCode.json")
return RevoltJson.decodeFromString(Changelog.serializer(), response.bodyAsText())
}
suspend fun getLatestChangelog(): ChangelogData {
return fetchChangelogIndex().changelogs.maxByOrNull { it.version.code }!!
}
suspend fun getLatestChangelogCode(): String {
return getLatestChangelog().version.code.toString()
}
suspend fun hasSeenCurrent(): Boolean {
if (kvStorage == null) {
throw IllegalStateException(
"Not supported for non-KVStorage instances of Changelogs"
)
}
return kvStorage.get("latestChangelogRead") == index.latest
val latest = getLatestChangelog().version.code
val appVersion = BuildConfig.VERSION_CODE
val appIsNewerThanLatestServerChangelog = appVersion > latest
// If the app is newer than the latest server changelog
if (appIsNewerThanLatestServerChangelog) {
return true
}
// Otherwise, check if the latest changelog has been read
return kvStorage.get("latestChangelogRead") == latest.toString()
}
suspend fun markAsSeen() {
@ -52,6 +102,8 @@ class Changelogs(val context: Context, val kvStorage: KVStorage? = null) {
)
}
kvStorage.set("latestChangelogRead", index.latest)
val index = fetchChangelogIndex()
val latest = index.changelogs.maxByOrNull { it.version.code }!!.version.code.toString()
kvStorage.set("latestChangelogRead", latest)
}
}

View File

@ -156,6 +156,7 @@ class ChatRouterViewModel @Inject constructor(
var sidebarSparkDisplayed by mutableStateOf(true)
var latestChangelogRead by mutableStateOf(true)
var latestChangelog by mutableStateOf("")
var latestChangelogBody by mutableStateOf("")
private val changelogs = Changelogs(context, kvStorage)
@ -170,8 +171,10 @@ class ChatRouterViewModel @Inject constructor(
kvStorage.getBoolean("sidebarSpark")!!
}
latestChangelogRead = changelogs.hasSeenLatest()
latestChangelog = changelogs.index.latest
latestChangelogRead = changelogs.hasSeenCurrent()
latestChangelog = changelogs.getLatestChangelogCode()
latestChangelogBody =
changelogs.fetchChangelogByVersionCode(latestChangelog.toLong()).rendered
if (!latestChangelogRead) {
changelogs.markAsSeen()
}
@ -423,19 +426,14 @@ fun ChatRouterScreen(
}
if (!viewModel.latestChangelogRead) {
val changelogSheetState = rememberModalBottomSheetState()
ModalBottomSheet(
sheetState = changelogSheetState,
onDismissRequest = {
ChangelogSheet(
versionName = viewModel.latestChangelog,
versionIsHistorical = false,
renderedContents = viewModel.latestChangelogBody,
onDismiss = {
viewModel.latestChangelogRead = true
}
) {
ChangelogSheet(
version = viewModel.latestChangelog,
new = true
)
}
)
}
if (showSidebarSpark.value) {

View File

@ -1,54 +1,79 @@
package chat.revolt.screens.settings
import android.content.Context
import android.text.format.DateUtils
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.input.nestedscroll.nestedScroll
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.lifecycle.viewModelScope
import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.internals.ChangelogIndex
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 kotlinx.coroutines.launch
import kotlinx.datetime.Instant
import javax.inject.Inject
@HiltViewModel
class ChangelogsSettingsScreenViewModel @Inject constructor(
kvStorage: KVStorage,
@ApplicationContext context: Context
val kvStorage: KVStorage,
@ApplicationContext val context: Context
) : ViewModel() {
private val changelogs = Changelogs(context, kvStorage)
val index = changelogs.index
val list = changelogs.getList()
var index by mutableStateOf<ChangelogIndex?>(null)
var renderedChangelog by mutableStateOf("")
suspend fun requestChangelog(version: String) {
viewModelScope.launch {
renderedChangelog = Changelogs(
context,
kvStorage
).fetchChangelogByVersionCode(version.toLong()).rendered
}
}
suspend fun populate() {
viewModelScope.launch {
index = Changelogs(context, kvStorage).fetchChangelogIndex()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@ -57,20 +82,31 @@ fun ChangelogsSettingsScreen(
navController: NavController,
viewModel: ChangelogsSettingsScreenViewModel = hiltViewModel()
) {
var currentChangelog by remember { mutableStateOf(viewModel.index.latest) }
LaunchedEffect(Unit) {
viewModel.populate()
}
var currentChangelog by remember { mutableStateOf("") }
var sheetOpen by remember { mutableStateOf(false) }
if (sheetOpen) {
val sheetState = rememberModalBottomSheetState()
LaunchedEffect(currentChangelog) {
if (currentChangelog.isNotEmpty())
viewModel.requestChangelog(currentChangelog)
}
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = {
if (sheetOpen) {
val changelog =
viewModel.index?.changelogs?.firstOrNull { it.version.code.toString() == currentChangelog }
?: return
ChangelogSheet(
versionName = changelog.version.name,
versionIsHistorical = true,
renderedContents = viewModel.renderedChangelog,
onDismiss = {
sheetOpen = false
}
) {
ChangelogSheet(version = currentChangelog)
}
)
}
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
@ -105,36 +141,61 @@ fun ChangelogsSettingsScreen(
.padding(pv)
.fillMaxSize()
) {
LazyColumn {
items(
viewModel.list.size,
key = { viewModel.list.keys.elementAt(it) }
) { index ->
val version = viewModel.list.keys.elementAt(index)
val changelog = viewModel.list[version]!!
Column(
Crossfade(targetState = viewModel.index, label = "index has items") { index ->
if (index == null) {
Box(
modifier = Modifier
.clickable {
currentChangelog = version
sheetOpen = true
}
.fillMaxWidth()
.height(200.dp)
) {
ListItem(
headlineContent = {
Text(
text = changelog.version,
style = MaterialTheme.typography.headlineSmall
)
},
supportingContent = {
Text(
text = changelog.summary,
)
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
} else {
LazyColumn {
items(
viewModel.index?.changelogs?.size ?: 0,
key = { index ->
viewModel.index?.changelogs?.get(index)?.version?.name ?: ""
}
)
HorizontalDivider()
) { index ->
val changelog = viewModel.index?.changelogs?.get(index) ?: return@items
val relativeTimeString = DateUtils.getRelativeTimeSpanString(
Instant.parse(changelog.date.publish).toEpochMilliseconds(),
System.currentTimeMillis(),
DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_ABBREV_ALL
)
Column(
modifier = Modifier
.clickable {
currentChangelog = changelog.version.code.toString()
sheetOpen = true
}
.fillMaxWidth()
) {
ListItem(
headlineContent = {
Text(
text = changelog.version.title,
modifier = Modifier.padding(bottom = 8.dp)
)
},
supportingContent = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = changelog.summary,
)
Text(
text = "${changelog.version.name} · $relativeTimeString",
modifier = Modifier.alpha(0.7f),
)
}
}
)
HorizontalDivider()
}
}
}
}
}

View File

@ -1,111 +1,35 @@
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.material3.MaterialTheme
import androidx.compose.material3.Text
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.SheetHeaderPadding
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)
}
}
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalContext
import chat.revolt.api.internals.getFragmentActivity
import chat.revolt.fragments.ChangelogBottomSheetFragment
@Composable
fun ChangelogSheet(
version: String,
new: Boolean = false,
viewModel: ChangelogSheetViewModel = hiltViewModel()
versionName: String,
versionIsHistorical: Boolean,
renderedContents: String,
onDismiss: () -> Unit
) {
LaunchedEffect(version) {
viewModel.populate(version)
}
val activity = LocalContext.current.getFragmentActivity()
Column {
SheetHeaderPadding {
Text(
text = 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)
)
},
style = MaterialTheme.typography.headlineSmall
DisposableEffect(versionName, renderedContents) {
val sheet = ChangelogBottomSheetFragment(onDismiss)
sheet.arguments =
ChangelogBottomSheetFragment.createArguments(
versionName,
versionIsHistorical,
renderedContents,
)
activity?.supportFragmentManager?.let {
sheet.show(it, ChangelogBottomSheetFragment.TAG)
}
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
)
}
onDispose {
sheet.dismiss()
}
}
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tv_title"
style="@style/TextAppearance.Material3.HeadlineSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp"
tools:text="Changelog for 1.0.0" />
<WebView
android:id="@+id/wv_changelog"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" />
</LinearLayout>