feat: use new changelogs system
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
278d8ac5de
commit
9bdada6fab
|
|
@ -1,90 +0,0 @@
|
||||||
# 
|
|
||||||
|
|
||||||
## 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!
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
# 
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
As one would expect, when a message consists solely of emotes, they will be displayed larger.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
This isn't a huge change, but it required some pretty heavy lifting on the development side.
|
|
||||||
|
|
||||||
## New Home Screen
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
# .png)
|
|
||||||
|
|
||||||
Hello Revolters! Continuing our roughly monthly release schedule, this is a big one.
|
|
||||||
|
|
||||||
## .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 👀
|
|
||||||
|
|
||||||
## 
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 
|
|
||||||
|
|
||||||
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?
|
|
||||||
|
|
||||||
## 
|
|
||||||
|
|
||||||
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 |
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -12,6 +12,13 @@
|
||||||
font-style: normal;
|
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-face {
|
||||||
font-family: "Inter";
|
font-family: "Inter";
|
||||||
src: url("/_android_res/font/inter_bold.ttf");
|
src: url("/_android_res/font/inter_bold.ttf");
|
||||||
|
|
@ -55,29 +62,23 @@
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="markdown"></div>
|
<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>
|
<script>
|
||||||
const renderMarkdown = () => {
|
const renderMarkdown = () => {
|
||||||
const markdown = document.querySelector("#markdown")
|
markdown.innerHTML = Bridge.getMarkdown()
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
window.renderMarkdown = renderMarkdown
|
window.renderMarkdown = renderMarkdown
|
||||||
|
|
||||||
|
|
@ -55,6 +55,7 @@ const val REVOLT_JANUARY = "https://jan.revolt.chat"
|
||||||
const val REVOLT_APP = "https://app.revolt.chat"
|
const val REVOLT_APP = "https://app.revolt.chat"
|
||||||
const val REVOLT_INVITES = "https://rvlt.gg"
|
const val REVOLT_INVITES = "https://rvlt.gg"
|
||||||
const val REVOLT_WEBSOCKET = "wss://ws.revolt.chat"
|
const val REVOLT_WEBSOCKET = "wss://ws.revolt.chat"
|
||||||
|
const val REVOLT_KJBOOK = "https://revoltchat.github.io/android"
|
||||||
|
|
||||||
fun buildUserAgent(accessMethod: String = "Ktor"): String {
|
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})"
|
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})"
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,16 @@ package chat.revolt.api.internals
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
|
||||||
fun Context.getComponentActivity(): ComponentActivity? = when (this) {
|
fun Context.getComponentActivity(): ComponentActivity? = when (this) {
|
||||||
is ComponentActivity -> this
|
is ComponentActivity -> this
|
||||||
is ContextWrapper -> baseContext.getComponentActivity()
|
is ContextWrapper -> baseContext.getComponentActivity()
|
||||||
else -> null
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.getFragmentActivity(): FragmentActivity? = when (this) {
|
||||||
|
is FragmentActivity -> this
|
||||||
|
is ContextWrapper -> baseContext.getFragmentActivity()
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package chat.revolt.components.generic
|
package chat.revolt.fragments
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
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.JavascriptInterface
|
||||||
import android.webkit.WebChromeClient
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
|
|
@ -11,65 +15,52 @@ import android.webkit.WebResourceResponse
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.core.content.res.ResourcesCompat
|
||||||
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.webkit.WebViewAssetLoader
|
import androidx.webkit.WebViewAssetLoader
|
||||||
|
import chat.revolt.R
|
||||||
import chat.revolt.activities.InviteActivity
|
import chat.revolt.activities.InviteActivity
|
||||||
import chat.revolt.api.REVOLT_APP
|
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
class ChangelogBottomSheetFragment(
|
||||||
* WebView-backed Markdown renderer that supports all Markdown features
|
val onDismiss: () -> Unit
|
||||||
* including KaTeX
|
) : BottomSheetDialogFragment() {
|
||||||
*/
|
private lateinit var binding: SheetChangelogBinding
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
@Composable
|
|
||||||
fun WebMarkdown(
|
|
||||||
text: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
maskLoading: Boolean = false,
|
|
||||||
simpleLineBreaks: Boolean = true,
|
|
||||||
) {
|
|
||||||
val contentColour = LocalContentColor.current
|
|
||||||
val materialColourScheme = MaterialTheme.colorScheme
|
|
||||||
|
|
||||||
var finishedLoading by remember { mutableStateOf(false) }
|
private fun argbAsCssColour(argb: Int): String {
|
||||||
|
val alpha = (argb shr 24 and 0xff) / 255.0f
|
||||||
if (!finishedLoading && maskLoading) {
|
val red = argb shr 16 and 0xff
|
||||||
Column(
|
val green = argb shr 8 and 0xff
|
||||||
modifier = Modifier.fillMaxWidth(),
|
val blue = argb and 0xff
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
return String.format("#%02x%02x%02x%02x", red, green, blue, (alpha * 255).toInt())
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AndroidView(
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
modifier = modifier,
|
override fun onCreateView(
|
||||||
factory = { context ->
|
inflater: LayoutInflater,
|
||||||
WebView(context).apply {
|
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()
|
val assetLoader = WebViewAssetLoader.Builder()
|
||||||
.setDomain(Uri.parse(REVOLT_APP).host!!)
|
.setDomain(Uri.parse(REVOLT_APP).host!!)
|
||||||
.addPathHandler(
|
.addPathHandler(
|
||||||
|
|
@ -119,7 +110,12 @@ fun WebMarkdown(
|
||||||
.setShowTitle(true)
|
.setShowTitle(true)
|
||||||
.setDefaultColorSchemeParams(
|
.setDefaultColorSchemeParams(
|
||||||
CustomTabColorSchemeParams.Builder()
|
CustomTabColorSchemeParams.Builder()
|
||||||
.setToolbarColor(materialColourScheme.background.toArgb())
|
.setToolbarColor(
|
||||||
|
MaterialColors.getColor(
|
||||||
|
binding.wvChangelog,
|
||||||
|
com.google.android.material.R.attr.backgroundColor
|
||||||
|
)
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
@ -131,7 +127,7 @@ fun WebMarkdown(
|
||||||
}
|
}
|
||||||
|
|
||||||
loadUrl(
|
loadUrl(
|
||||||
"$REVOLT_APP/_android_assets/webmarkdown/renderer.html"
|
"$REVOLT_APP/_android_assets/changelogs/renderer.html"
|
||||||
)
|
)
|
||||||
|
|
||||||
settings.apply {
|
settings.apply {
|
||||||
|
|
@ -145,46 +141,62 @@ fun WebMarkdown(
|
||||||
|
|
||||||
addJavascriptInterface(
|
addJavascriptInterface(
|
||||||
object {
|
object {
|
||||||
@JavascriptInterface
|
|
||||||
fun onLoaded() {
|
|
||||||
finishedLoading = true
|
|
||||||
}
|
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun getMarkdown(): String {
|
fun getMarkdown(): String {
|
||||||
return text
|
return getString(ARG_RENDERED_CONTENTS) ?: ""
|
||||||
.replace("&", "&")
|
|
||||||
.replace("<", "<")
|
|
||||||
.replace(">", ">")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun getContentColour(): String {
|
fun getContentColour(): String {
|
||||||
return argbAsCssColour(contentColour.toArgb())
|
return argbAsCssColour(
|
||||||
|
MaterialColors.getColor(
|
||||||
|
binding.wvChangelog,
|
||||||
|
com.google.android.material.R.attr.colorOnSurface
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun getPrimaryColour(): String {
|
fun getPrimaryColour(): String {
|
||||||
return argbAsCssColour(materialColourScheme.primary.toArgb())
|
return argbAsCssColour(
|
||||||
}
|
MaterialColors.getColor(
|
||||||
|
binding.wvChangelog,
|
||||||
@JavascriptInterface
|
com.google.android.material.R.attr.colorPrimary
|
||||||
fun shouldUseSimpleLineBreaks(): Boolean {
|
)
|
||||||
return simpleLineBreaks
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Bridge"
|
"Bridge"
|
||||||
)
|
)
|
||||||
|
|
||||||
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,48 +1,98 @@
|
||||||
package chat.revolt.internals
|
package chat.revolt.internals
|
||||||
|
|
||||||
import android.content.Context
|
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.api.RevoltJson
|
||||||
|
import chat.revolt.internals.IndexHolder.cachedIndex
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Changelog(
|
data class ChangelogIndex(
|
||||||
val summary: String,
|
val changelogs: List<ChangelogData> = emptyList()
|
||||||
val version: String,
|
|
||||||
val date: String
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ChangelogIndex(
|
data class ChangelogData(
|
||||||
val list: Map<String, Changelog>,
|
val version: ChangelogVersion,
|
||||||
val latest: String
|
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) {
|
class Changelogs(val context: Context, val kvStorage: KVStorage? = null) {
|
||||||
val index = context.assets.open("changelogs/index.json").use {
|
suspend fun fetchChangelogIndex(): ChangelogIndex {
|
||||||
it.reader().readText()
|
if (cachedIndex != null) {
|
||||||
}.let {
|
return cachedIndex as ChangelogIndex
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (kvStorage == null) {
|
||||||
throw IllegalStateException(
|
throw IllegalStateException(
|
||||||
"Not supported for non-KVStorage instances of Changelogs"
|
"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() {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@ class ChatRouterViewModel @Inject constructor(
|
||||||
var sidebarSparkDisplayed by mutableStateOf(true)
|
var sidebarSparkDisplayed by mutableStateOf(true)
|
||||||
var latestChangelogRead by mutableStateOf(true)
|
var latestChangelogRead by mutableStateOf(true)
|
||||||
var latestChangelog by mutableStateOf("")
|
var latestChangelog by mutableStateOf("")
|
||||||
|
var latestChangelogBody by mutableStateOf("")
|
||||||
|
|
||||||
private val changelogs = Changelogs(context, kvStorage)
|
private val changelogs = Changelogs(context, kvStorage)
|
||||||
|
|
||||||
|
|
@ -170,8 +171,10 @@ class ChatRouterViewModel @Inject constructor(
|
||||||
kvStorage.getBoolean("sidebarSpark")!!
|
kvStorage.getBoolean("sidebarSpark")!!
|
||||||
}
|
}
|
||||||
|
|
||||||
latestChangelogRead = changelogs.hasSeenLatest()
|
latestChangelogRead = changelogs.hasSeenCurrent()
|
||||||
latestChangelog = changelogs.index.latest
|
latestChangelog = changelogs.getLatestChangelogCode()
|
||||||
|
latestChangelogBody =
|
||||||
|
changelogs.fetchChangelogByVersionCode(latestChangelog.toLong()).rendered
|
||||||
if (!latestChangelogRead) {
|
if (!latestChangelogRead) {
|
||||||
changelogs.markAsSeen()
|
changelogs.markAsSeen()
|
||||||
}
|
}
|
||||||
|
|
@ -423,19 +426,14 @@ fun ChatRouterScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!viewModel.latestChangelogRead) {
|
if (!viewModel.latestChangelogRead) {
|
||||||
val changelogSheetState = rememberModalBottomSheetState()
|
ChangelogSheet(
|
||||||
|
versionName = viewModel.latestChangelog,
|
||||||
ModalBottomSheet(
|
versionIsHistorical = false,
|
||||||
sheetState = changelogSheetState,
|
renderedContents = viewModel.latestChangelogBody,
|
||||||
onDismissRequest = {
|
onDismiss = {
|
||||||
viewModel.latestChangelogRead = true
|
viewModel.latestChangelogRead = true
|
||||||
}
|
}
|
||||||
) {
|
)
|
||||||
ChangelogSheet(
|
|
||||||
version = viewModel.latestChangelog,
|
|
||||||
new = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showSidebarSpark.value) {
|
if (showSidebarSpark.value) {
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,79 @@
|
||||||
package chat.revolt.screens.settings
|
package chat.revolt.screens.settings
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.clickable
|
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.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LargeTopAppBar
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import chat.revolt.R
|
import chat.revolt.R
|
||||||
|
import chat.revolt.internals.ChangelogIndex
|
||||||
import chat.revolt.internals.Changelogs
|
import chat.revolt.internals.Changelogs
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
import chat.revolt.sheets.ChangelogSheet
|
import chat.revolt.sheets.ChangelogSheet
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ChangelogsSettingsScreenViewModel @Inject constructor(
|
class ChangelogsSettingsScreenViewModel @Inject constructor(
|
||||||
kvStorage: KVStorage,
|
val kvStorage: KVStorage,
|
||||||
@ApplicationContext context: Context
|
@ApplicationContext val context: Context
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val changelogs = Changelogs(context, kvStorage)
|
var index by mutableStateOf<ChangelogIndex?>(null)
|
||||||
val index = changelogs.index
|
var renderedChangelog by mutableStateOf("")
|
||||||
val list = changelogs.getList()
|
|
||||||
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
@ -57,20 +82,31 @@ fun ChangelogsSettingsScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
viewModel: ChangelogsSettingsScreenViewModel = hiltViewModel()
|
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) }
|
var sheetOpen by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if (sheetOpen) {
|
LaunchedEffect(currentChangelog) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
if (currentChangelog.isNotEmpty())
|
||||||
|
viewModel.requestChangelog(currentChangelog)
|
||||||
|
}
|
||||||
|
|
||||||
ModalBottomSheet(
|
if (sheetOpen) {
|
||||||
sheetState = sheetState,
|
val changelog =
|
||||||
onDismissRequest = {
|
viewModel.index?.changelogs?.firstOrNull { it.version.code.toString() == currentChangelog }
|
||||||
|
?: return
|
||||||
|
|
||||||
|
ChangelogSheet(
|
||||||
|
versionName = changelog.version.name,
|
||||||
|
versionIsHistorical = true,
|
||||||
|
renderedContents = viewModel.renderedChangelog,
|
||||||
|
onDismiss = {
|
||||||
sheetOpen = false
|
sheetOpen = false
|
||||||
}
|
}
|
||||||
) {
|
)
|
||||||
ChangelogSheet(version = currentChangelog)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
|
@ -105,36 +141,61 @@ fun ChangelogsSettingsScreen(
|
||||||
.padding(pv)
|
.padding(pv)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
LazyColumn {
|
Crossfade(targetState = viewModel.index, label = "index has items") { index ->
|
||||||
items(
|
if (index == null) {
|
||||||
viewModel.list.size,
|
Box(
|
||||||
key = { viewModel.list.keys.elementAt(it) }
|
|
||||||
) { index ->
|
|
||||||
val version = viewModel.list.keys.elementAt(index)
|
|
||||||
val changelog = viewModel.list[version]!!
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
|
||||||
currentChangelog = version
|
|
||||||
sheetOpen = true
|
|
||||||
}
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.height(200.dp)
|
||||||
) {
|
) {
|
||||||
ListItem(
|
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||||
headlineContent = {
|
}
|
||||||
Text(
|
} else {
|
||||||
text = changelog.version,
|
LazyColumn {
|
||||||
style = MaterialTheme.typography.headlineSmall
|
items(
|
||||||
)
|
viewModel.index?.changelogs?.size ?: 0,
|
||||||
},
|
key = { index ->
|
||||||
supportingContent = {
|
viewModel.index?.changelogs?.get(index)?.version?.name ?: ""
|
||||||
Text(
|
|
||||||
text = changelog.summary,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
) { index ->
|
||||||
HorizontalDivider()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,111 +1,35 @@
|
||||||
package chat.revolt.sheets
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.runtime.key
|
import chat.revolt.api.internals.getFragmentActivity
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import chat.revolt.fragments.ChangelogBottomSheetFragment
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChangelogSheet(
|
fun ChangelogSheet(
|
||||||
version: String,
|
versionName: String,
|
||||||
new: Boolean = false,
|
versionIsHistorical: Boolean,
|
||||||
viewModel: ChangelogSheetViewModel = hiltViewModel()
|
renderedContents: String,
|
||||||
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(version) {
|
val activity = LocalContext.current.getFragmentActivity()
|
||||||
viewModel.populate(version)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
DisposableEffect(versionName, renderedContents) {
|
||||||
SheetHeaderPadding {
|
val sheet = ChangelogBottomSheetFragment(onDismiss)
|
||||||
Text(
|
sheet.arguments =
|
||||||
text = if (new) {
|
ChangelogBottomSheetFragment.createArguments(
|
||||||
stringResource(R.string.settings_changelogs_new_header)
|
versionName,
|
||||||
} else {
|
versionIsHistorical,
|
||||||
stringResource(
|
renderedContents,
|
||||||
R.string.settings_changelogs_historical_version_header,
|
|
||||||
viewModel.changelog?.version
|
|
||||||
?: stringResource(R.string.settings_changelogs_historical_version_header_placeholder)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.headlineSmall
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
activity?.supportFragmentManager?.let {
|
||||||
|
sheet.show(it, ChangelogBottomSheetFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewModel.changelogContents == null) {
|
onDispose {
|
||||||
Box(
|
sheet.dismiss()
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue