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-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
|
||||
|
||||
|
|
@ -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})"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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