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