style: run ktlint on code

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2023-10-21 16:27:05 +02:00
parent f15438708e
commit fac7411eea
140 changed files with 1230 additions and 989 deletions

2
.editorconfig Normal file
View File

@ -0,0 +1,2 @@
[*.{kt,kts}]
ktlint_code_style = android_studio

View File

@ -14,4 +14,4 @@ class RevoltApplication : Application() {
instance = this instance = this
DynamicColors.applyToActivitiesIfAvailable(this) DynamicColors.applyToActivitiesIfAvailable(this)
} }
} }

View File

@ -180,7 +180,7 @@ fun InviteScreen(
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize()
) )
Box( Box(
@ -214,11 +214,10 @@ fun InviteScreen(
?: stringResource(id = R.string.unknown), ?: stringResource(id = R.string.unknown),
modifier = Modifier modifier = Modifier
.size(64.dp) .size(64.dp)
.clip(CircleShape), .clip(CircleShape)
) )
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
@ -227,7 +226,7 @@ fun InviteScreen(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 24.sp, fontSize = 24.sp,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -235,7 +234,7 @@ fun InviteScreen(
Text( Text(
text = stringResource(id = R.string.invite_message), text = stringResource(id = R.string.invite_message),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@ -270,10 +269,7 @@ fun InviteScreen(
} }
@Composable @Composable
fun InvalidInviteError( fun InvalidInviteError(error: RevoltError? = null, onDismissRequest: () -> Unit) {
error: RevoltError? = null,
onDismissRequest: () -> Unit
) {
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
icon = { icon = {
@ -287,11 +283,11 @@ fun InvalidInviteError(
Text( Text(
text = stringResource(id = R.string.invite_error_header), text = stringResource(id = R.string.invite_error_header),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
) )
}, },
text = { text = {
Column() { Column {
Text( Text(
text = stringResource( text = stringResource(
id = when (error?.type) { id = when (error?.type) {
@ -301,7 +297,7 @@ fun InvalidInviteError(
} }
), ),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
) )
} }
}, },
@ -333,15 +329,15 @@ fun NoInviteSpecifiedError(onDismissRequest: () -> Unit) {
Text( Text(
text = stringResource(id = R.string.invite_error_header), text = stringResource(id = R.string.invite_error_header),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
) )
}, },
text = { text = {
Column() { Column {
Text( Text(
text = stringResource(id = R.string.invite_error_no_invite), text = stringResource(id = R.string.invite_error_no_invite),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth()
) )
} }
}, },
@ -356,4 +352,4 @@ fun NoInviteSpecifiedError(onDismissRequest: () -> Unit) {
}, },
confirmButton = {} confirmButton = {}
) )
} }

View File

@ -84,7 +84,7 @@ fun AppEntrypoint(windowSizeClass: WindowSizeClass) {
val navController = rememberNavController() val navController = rememberNavController()
RevoltTheme( RevoltTheme(
requestedTheme = GlobalState.theme, requestedTheme = GlobalState.theme
) { ) {
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),

View File

@ -49,4 +49,4 @@ class WebChallengeActivity : AppCompatActivity() {
binding.webView.loadUrl(REVOLT_BASE) binding.webView.loadUrl(REVOLT_BASE)
} }
} }

View File

@ -79,10 +79,7 @@ class ImageViewActivity : ComponentActivity() {
} }
@Composable @Composable
fun ImageViewScreen( fun ImageViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
resource: AutumnResource,
onClose: () -> Unit = {}
) {
val resourceUrl = "$REVOLT_FILES/attachments/${resource.id}/${resource.filename}" val resourceUrl = "$REVOLT_FILES/attachments/${resource.id}/${resource.filename}"
val context = LocalContext.current val context = LocalContext.current
@ -182,7 +179,7 @@ fun ImageViewScreen(
RevoltTheme(requestedTheme = GlobalState.theme) { RevoltTheme(requestedTheme = GlobalState.theme) {
Scaffold( Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
) { pv -> ) { pv ->
Surface( Surface(
modifier = Modifier modifier = Modifier
@ -191,10 +188,11 @@ fun ImageViewScreen(
.fillMaxSize() .fillMaxSize()
) { ) {
Column { Column {
PageHeader(text = stringResource( PageHeader(
id = R.string.media_viewer_title_image, text = stringResource(
resource.filename ?: resource.id!! id = R.string.media_viewer_title_image,
), resource.filename ?: resource.id!!
),
showBackButton = true, showBackButton = true,
onBackButtonClicked = onClose, onBackButtonClicked = onClose,
maxLines = 1, maxLines = 1,
@ -213,13 +211,16 @@ fun ImageViewScreen(
expanded = shareSubmenuIsOpen.value, expanded = shareSubmenuIsOpen.value,
onDismissRequest = { onDismissRequest = {
shareSubmenuIsOpen.value = false shareSubmenuIsOpen.value = false
}) { }
) {
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
shareUrl() shareUrl()
}, },
text = { text = {
Text(stringResource(id = R.string.media_viewer_share_url)) Text(
stringResource(id = R.string.media_viewer_share_url)
)
} }
) )
DropdownMenuItem( DropdownMenuItem(
@ -227,7 +228,11 @@ fun ImageViewScreen(
shareImage() shareImage()
}, },
text = { text = {
Text(stringResource(id = R.string.media_viewer_share_image)) Text(
stringResource(
id = R.string.media_viewer_share_image
)
)
} }
) )
} }
@ -237,11 +242,14 @@ fun ImageViewScreen(
}) { }) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_download_24dp), painter = painterResource(id = R.drawable.ic_download_24dp),
contentDescription = stringResource(id = R.string.media_viewer_save) contentDescription = stringResource(
id = R.string.media_viewer_save
)
) )
} }
} }
}) }
)
Box( Box(
modifier = Modifier modifier = Modifier
@ -260,11 +268,11 @@ fun ImageViewScreen(
}, },
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background), .background(MaterialTheme.colorScheme.background)
) )
} }
} }
} }
} }
} }
} }

View File

@ -83,10 +83,7 @@ class VideoViewActivity : ComponentActivity() {
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
@Composable @Composable
fun VideoViewScreen( fun VideoViewScreen(resource: AutumnResource, onClose: () -> Unit = {}) {
resource: AutumnResource,
onClose: () -> Unit = {}
) {
val resourceUrl = "$REVOLT_FILES/attachments/${resource.id}/${resource.filename}" val resourceUrl = "$REVOLT_FILES/attachments/${resource.id}/${resource.filename}"
val context = LocalContext.current val context = LocalContext.current
@ -198,7 +195,7 @@ fun VideoViewScreen(
RevoltTheme(requestedTheme = GlobalState.theme) { RevoltTheme(requestedTheme = GlobalState.theme) {
Scaffold( Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
) { pv -> ) { pv ->
Surface( Surface(
modifier = Modifier modifier = Modifier
@ -207,10 +204,11 @@ fun VideoViewScreen(
.fillMaxSize() .fillMaxSize()
) { ) {
Column { Column {
PageHeader(text = stringResource( PageHeader(
id = R.string.media_viewer_title_video, text = stringResource(
resource.filename ?: resource.id!! id = R.string.media_viewer_title_video,
), resource.filename ?: resource.id!!
),
showBackButton = true, showBackButton = true,
onBackButtonClicked = onClose, onBackButtonClicked = onClose,
maxLines = 1, maxLines = 1,
@ -229,13 +227,16 @@ fun VideoViewScreen(
expanded = shareSubmenuIsOpen.value, expanded = shareSubmenuIsOpen.value,
onDismissRequest = { onDismissRequest = {
shareSubmenuIsOpen.value = false shareSubmenuIsOpen.value = false
}) { }
) {
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
shareUrl() shareUrl()
}, },
text = { text = {
Text(stringResource(id = R.string.media_viewer_share_url)) Text(
stringResource(id = R.string.media_viewer_share_url)
)
} }
) )
DropdownMenuItem( DropdownMenuItem(
@ -243,7 +244,11 @@ fun VideoViewScreen(
shareVideo() shareVideo()
}, },
text = { text = {
Text(stringResource(id = R.string.media_viewer_share_video)) Text(
stringResource(
id = R.string.media_viewer_share_video
)
)
} }
) )
} }
@ -253,11 +258,14 @@ fun VideoViewScreen(
}) { }) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_download_24dp), painter = painterResource(id = R.drawable.ic_download_24dp),
contentDescription = stringResource(id = R.string.media_viewer_save) contentDescription = stringResource(
id = R.string.media_viewer_save
)
) )
} }
} }
}) }
)
Box( Box(
modifier = Modifier modifier = Modifier
@ -275,11 +283,11 @@ fun VideoViewScreen(
}, },
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background), .background(MaterialTheme.colorScheme.background)
) )
} }
} }
} }
} }
} }
} }

View File

@ -9,6 +9,7 @@ import chat.revolt.api.internals.Members
import chat.revolt.api.realtime.DisconnectionState import chat.revolt.api.realtime.DisconnectionState
import chat.revolt.api.realtime.RealtimeSocket import chat.revolt.api.realtime.RealtimeSocket
import chat.revolt.api.routes.user.fetchSelf import chat.revolt.api.routes.user.fetchSelf
import chat.revolt.api.schemas.Channel as ChannelSchema
import chat.revolt.api.schemas.Emoji import chat.revolt.api.schemas.Emoji
import chat.revolt.api.schemas.Message import chat.revolt.api.schemas.Message
import chat.revolt.api.schemas.Server import chat.revolt.api.schemas.Server
@ -38,7 +39,6 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import chat.revolt.api.schemas.Channel as ChannelSchema
const val REVOLT_BASE = "https://api.revolt.chat" const val REVOLT_BASE = "https://api.revolt.chat"
const val REVOLT_SUPPORT = "https://support.revolt.chat" const val REVOLT_SUPPORT = "https://support.revolt.chat"
@ -218,4 +218,4 @@ object RevoltAPI {
} }
@Serializable @Serializable
data class RevoltError(val type: String) data class RevoltError(val type: String)

View File

@ -50,4 +50,4 @@ object ChannelUtils {
return output return output
} }
} }

View File

@ -21,14 +21,14 @@ object DirectMessages {
fun hasPlatformModerationDM(): Boolean { fun hasPlatformModerationDM(): Boolean {
return unreadDMs().any { return unreadDMs().any {
it.channelType == ChannelType.DirectMessage && it.channelType == ChannelType.DirectMessage &&
it.recipients?.contains(PLATFORM_MODERATION_USER) ?: false it.recipients?.contains(PLATFORM_MODERATION_USER) ?: false
} }
} }
fun getPlatformModerationDM(): Channel? { fun getPlatformModerationDM(): Channel? {
return unreadDMs().firstOrNull { return unreadDMs().firstOrNull {
it.channelType == ChannelType.DirectMessage && it.channelType == ChannelType.DirectMessage &&
it.recipients?.contains(PLATFORM_MODERATION_USER) ?: false it.recipients?.contains(PLATFORM_MODERATION_USER) ?: false
} }
} }
} }

View File

@ -38,4 +38,4 @@ class Members {
member.nickname?.let { userId to member.nickname } member.nickname?.let { userId to member.nickname }
}?.toMap() ?: emptyMap() }?.toMap() ?: emptyMap()
} }
} }

View File

@ -76,30 +76,30 @@ object BitDefaults {
val Default = val Default =
ViewOnly + ViewOnly +
PermissionBit.SendMessage + PermissionBit.SendMessage +
PermissionBit.InviteOthers + PermissionBit.InviteOthers +
PermissionBit.SendEmbeds + PermissionBit.SendEmbeds +
PermissionBit.UploadFiles + PermissionBit.UploadFiles +
PermissionBit.Connect + PermissionBit.Connect +
PermissionBit.Speak PermissionBit.Speak
val SavedMessages = val SavedMessages =
PermissionBit.GrantAllSafe.value PermissionBit.GrantAllSafe.value
val DirectMessages = val DirectMessages =
Default + Default +
PermissionBit.ManageChannel + PermissionBit.ManageChannel +
PermissionBit.React PermissionBit.React
val Server = val Server =
Default + Default +
PermissionBit.React + PermissionBit.React +
PermissionBit.ChangeNickname + PermissionBit.ChangeNickname +
PermissionBit.ChangeAvatar PermissionBit.ChangeAvatar
val Webhook = val Webhook =
PermissionBit.SendMessage + PermissionBit.SendMessage +
PermissionBit.SendEmbeds + PermissionBit.SendEmbeds +
PermissionBit.Masquerade + PermissionBit.Masquerade +
PermissionBit.React PermissionBit.React
} }

View File

@ -80,7 +80,7 @@ object Roles {
ChannelType.TextChannel, ChannelType.VoiceChannel -> { ChannelType.TextChannel, ChannelType.VoiceChannel -> {
val server = RevoltAPI.serverCache[channel.server] val server = RevoltAPI.serverCache[channel.server]
// FIXME this is a stupid patch to prevent it from showing "no permission" on a channel on launch // FIXME this is a stupid patch to prevent it from showing "no permission" on a channel on launch
?: return PermissionBit.GrantAllSafe.value ?: return PermissionBit.GrantAllSafe.value
if (server.owner == user?.id) return PermissionBit.GrantAllSafe.value if (server.owner == user?.id) return PermissionBit.GrantAllSafe.value
@ -114,4 +114,4 @@ object Roles {
null -> 0L null -> 0L
} }
} }
} }

View File

@ -4,17 +4,17 @@ import android.content.Context
import android.graphics.RuntimeShader import android.graphics.RuntimeShader
import android.os.Build import android.os.Build
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush as AndroidBrush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.ShaderBrush
import org.intellij.lang.annotations.Language import org.intellij.lang.annotations.Language
import androidx.compose.ui.graphics.Brush as AndroidBrush
object SpecialUsers { object SpecialUsers {
val PLATFORM_MODERATION_USER = "01FC17E1WTM2BGE4F3ARN3FDAF" val PLATFORM_MODERATION_USER = "01FC17E1WTM2BGE4F3ARN3FDAF"
val TRUSTED_MODERATION_BOTS = listOf( val TRUSTED_MODERATION_BOTS = listOf(
"01GXBYCNQ52A9QYCQ99RBPXPAW", // AutoMod "01GXBYCNQ52A9QYCQ99RBPXPAW", // AutoMod
"01FCXRNNVW69AMSHBE61W1M5T3", // AutoMod Nightly "01FCXRNNVW69AMSHBE61W1M5T3" // AutoMod Nightly
) )
sealed class TeamMemberFlair { sealed class TeamMemberFlair {
@ -40,7 +40,7 @@ object SpecialUsers {
Color(0xFFFF9B55), Color(0xFFFF9B55),
Color(0xFFFFFFFF), Color(0xFFFFFFFF),
Color(0xFFD461A6), Color(0xFFD461A6),
Color(0xFFA50062), Color(0xFFA50062)
), ),
start = Offset.Zero, start = Offset.Zero,
end = Offset.Infinite end = Offset.Infinite
@ -51,7 +51,7 @@ object SpecialUsers {
AndroidBrush.linearGradient( AndroidBrush.linearGradient(
listOf( listOf(
Color(0xFF68224F), Color(0xFF68224F),
Color(0xFFC68235), Color(0xFFC68235)
), ),
start = Offset.Zero, start = Offset.Zero,
end = Offset.Infinite end = Offset.Infinite
@ -75,7 +75,7 @@ object SpecialUsers {
Color(0xFF1000AF) Color(0xFF1000AF)
) )
) )
), // zomatree ) // zomatree
) )
fun teamFlairAsBrush(context: Context, id: String): AndroidBrush? { fun teamFlairAsBrush(context: Context, id: String): AndroidBrush? {
@ -93,4 +93,4 @@ object SpecialUsers {
null -> null null -> null
} }
} }
} }

View File

@ -45,35 +45,67 @@ object ULID {
// Entropy part (16 chars) // Entropy part (16 chars)
chars[10] = b32chars[(entropy[0].toShort() and 0xff).toInt().ushr(3)] chars[10] = b32chars[(entropy[0].toShort() and 0xff).toInt().ushr(3)]
chars[11] = chars[11] =
b32chars[(entropy[0].toInt() shl 2 or (entropy[1].toShort() and 0xff).toInt() b32chars[
.ushr(6) and 0x1f)] (
entropy[0].toInt() shl 2 or (entropy[1].toShort() and 0xff).toInt()
.ushr(6) and 0x1f
)
]
chars[12] = b32chars[((entropy[1].toShort() and 0xff).toInt().ushr(1) and 0x1f)] chars[12] = b32chars[((entropy[1].toShort() and 0xff).toInt().ushr(1) and 0x1f)]
chars[13] = chars[13] =
b32chars[(entropy[1].toInt() shl 4 or (entropy[2].toShort() and 0xff).toInt() b32chars[
.ushr(4) and 0x1f)] (
entropy[1].toInt() shl 4 or (entropy[2].toShort() and 0xff).toInt()
.ushr(4) and 0x1f
)
]
chars[14] = chars[14] =
b32chars[(entropy[2].toInt() shl 5 or (entropy[3].toShort() and 0xff).toInt() b32chars[
.ushr(7) and 0x1f)] (
entropy[2].toInt() shl 5 or (entropy[3].toShort() and 0xff).toInt()
.ushr(7) and 0x1f
)
]
chars[15] = b32chars[((entropy[3].toShort() and 0xff).toInt().ushr(2) and 0x1f)] chars[15] = b32chars[((entropy[3].toShort() and 0xff).toInt().ushr(2) and 0x1f)]
chars[16] = chars[16] =
b32chars[(entropy[3].toInt() shl 3 or (entropy[4].toShort() and 0xff).toInt() b32chars[
.ushr(5) and 0x1f)] (
entropy[3].toInt() shl 3 or (entropy[4].toShort() and 0xff).toInt()
.ushr(5) and 0x1f
)
]
chars[17] = b32chars[(entropy[4].toInt() and 0x1f)] chars[17] = b32chars[(entropy[4].toInt() and 0x1f)]
chars[18] = b32chars[(entropy[5].toShort() and 0xff).toInt().ushr(3)] chars[18] = b32chars[(entropy[5].toShort() and 0xff).toInt().ushr(3)]
chars[19] = chars[19] =
b32chars[(entropy[5].toInt() shl 2 or (entropy[6].toShort() and 0xff).toInt() b32chars[
.ushr(6) and 0x1f)] (
entropy[5].toInt() shl 2 or (entropy[6].toShort() and 0xff).toInt()
.ushr(6) and 0x1f
)
]
chars[20] = b32chars[((entropy[6].toShort() and 0xff).toInt().ushr(1) and 0x1f)] chars[20] = b32chars[((entropy[6].toShort() and 0xff).toInt().ushr(1) and 0x1f)]
chars[21] = chars[21] =
b32chars[(entropy[6].toInt() shl 4 or (entropy[7].toShort() and 0xff).toInt() b32chars[
.ushr(4) and 0x1f)] (
entropy[6].toInt() shl 4 or (entropy[7].toShort() and 0xff).toInt()
.ushr(4) and 0x1f
)
]
chars[22] = chars[22] =
b32chars[(entropy[7].toInt() shl 5 or (entropy[8].toShort() and 0xff).toInt() b32chars[
.ushr(7) and 0x1f)] (
entropy[7].toInt() shl 5 or (entropy[8].toShort() and 0xff).toInt()
.ushr(7) and 0x1f
)
]
chars[23] = b32chars[((entropy[8].toShort() and 0xff).toInt().ushr(2) and 0x1f)] chars[23] = b32chars[((entropy[8].toShort() and 0xff).toInt().ushr(2) and 0x1f)]
chars[24] = chars[24] =
b32chars[(entropy[8].toInt() shl 3 or (entropy[9].toShort() and 0xff).toInt() b32chars[
.ushr(5) and 0x1f)] (
entropy[8].toInt() shl 3 or (entropy[9].toShort() and 0xff).toInt()
.ushr(5) and 0x1f
)
]
chars[25] = b32chars[(entropy[9].toInt() and 0x1f)] chars[25] = b32chars[(entropy[9].toInt() and 0x1f)]
return String(chars) return String(chars)
@ -109,4 +141,4 @@ object ULID {
return timestamp return timestamp
} }
} }

View File

@ -9,7 +9,9 @@ object WebChallenge {
suspend fun needsCloudflare(): Boolean { suspend fun needsCloudflare(): Boolean {
RevoltHttp.get(REVOLT_BASE).let { RevoltHttp.get(REVOLT_BASE).let {
val text = it.bodyAsText() val text = it.bodyAsText()
return text.contains("window._cf_chl_opt") // FIXME Naive, prone to captcha page changing return text.contains(
"window._cf_chl_opt"
) // FIXME Naive, prone to captcha page changing
} }
} }
} }

View File

@ -26,7 +26,7 @@ private val ADDITIONAL_WEB_COLOURS = mapOf(
"transparent" to Color.Transparent, "transparent" to Color.Transparent,
"inherit" to Color.Unspecified, "inherit" to Color.Unspecified,
"initial" to Color.Unspecified, "initial" to Color.Unspecified,
"unset" to Color.Unspecified, "unset" to Color.Unspecified
) )
object WebCompat { object WebCompat {
@ -199,4 +199,4 @@ object WebCompat {
} }
} }
} }
} }

View File

@ -429,4 +429,4 @@ object RealtimeSocket {
private suspend fun pushReconnectEvent() { private suspend fun pushReconnectEvent() {
RevoltAPI.wsFrameChannel.send(RealtimeSocketFrames.Reconnected()) RevoltAPI.wsFrameChannel.send(RealtimeSocketFrames.Reconnected())
} }
} }

View File

@ -7,13 +7,13 @@ import kotlinx.serialization.json.JsonObject
@Serializable @Serializable
data class AnyFrame( data class AnyFrame(
val type: String, val type: String
) )
@Serializable @Serializable
data class ErrorFrame( data class ErrorFrame(
val type: String = "Error", val type: String = "Error",
val error: String, val error: String
) )
@Serializable @Serializable
@ -49,7 +49,7 @@ data class MessageUpdateFrame(
@Serializable @Serializable
data class Appendable( data class Appendable(
val embeds: List<Embed>? = null, val embeds: List<Embed>? = null
) )
@Serializable @Serializable
@ -73,7 +73,7 @@ data class MessageReactFrame(
val id: String, val id: String,
val channel_id: String, val channel_id: String,
val user_id: String, val user_id: String,
val emoji_id: String, val emoji_id: String
) )
@Serializable @Serializable
@ -82,7 +82,7 @@ data class MessageUnreactFrame(
val id: String, val id: String,
val channel_id: String, val channel_id: String,
val user_id: String, val user_id: String,
val emoji_id: String, val emoji_id: String
) )
@Serializable @Serializable
@ -90,7 +90,7 @@ data class MessageRemoveReactionFrame(
val type: String = "MessageRemoveReaction", val type: String = "MessageRemoveReaction",
val id: String, val id: String,
val channel_id: String, val channel_id: String,
val emoji_id: String, val emoji_id: String
) )
/* ChannelCreate: we already have a "type" property in channel so we just alias the type */ /* ChannelCreate: we already have a "type" property in channel so we just alias the type */
@ -152,7 +152,7 @@ data class ServerCreateFrame(
val type: String = "ServerCreate", val type: String = "ServerCreate",
val id: String, val id: String,
val server: Server, val server: Server,
val channels: List<Channel>, val channels: List<Channel>
) )
@Serializable @Serializable
@ -222,7 +222,7 @@ data class UserRelationshipFrame(
val type: String = "UserRelationship", val type: String = "UserRelationship",
val id: String, val id: String,
val user: User, val user: User,
val status: String, val status: String
) )
typealias EmojiCreateFrame = Emoji typealias EmojiCreateFrame = Emoji
@ -230,5 +230,5 @@ typealias EmojiCreateFrame = Emoji
@Serializable @Serializable
data class EmojiDeleteFrame( data class EmojiDeleteFrame(
val type: String = "EmojiDelete", val type: String = "EmojiDelete",
val id: String, val id: String
) )

View File

@ -17,7 +17,7 @@ data class LoginNegotiation(
@SerialName("friendly_name") @SerialName("friendly_name")
val friendlyName: String, val friendlyName: String,
val captcha: String? = null, val captcha: String? = null
) )
@Serializable @Serializable
@ -111,7 +111,6 @@ suspend fun negotiateAuthentication(email: String, password: String): EmailPassw
val responseContent = response.bodyAsText() val responseContent = response.bodyAsText()
Log.d("Revolt", "negotiateAuthentication: $responseContent") Log.d("Revolt", "negotiateAuthentication: $responseContent")
try { try {
val error = RevoltJson.decodeFromString(RevoltError.serializer(), responseContent) val error = RevoltJson.decodeFromString(RevoltError.serializer(), responseContent)
return EmailPasswordAssessment(error = error) return EmailPasswordAssessment(error = error)
@ -122,7 +121,7 @@ suspend fun negotiateAuthentication(email: String, password: String): EmailPassw
if (response.status == HttpStatusCode.InternalServerError) { if (response.status == HttpStatusCode.InternalServerError) {
return EmailPasswordAssessment( return EmailPasswordAssessment(
error = RevoltError( error = RevoltError(
"InternalServerError", "InternalServerError"
) )
) )
} }
@ -143,7 +142,7 @@ suspend fun negotiateAuthentication(email: String, password: String): EmailPassw
suspend fun authenticateWithMfaTotpCode( suspend fun authenticateWithMfaTotpCode(
mfaTicket: String, mfaTicket: String,
mfaResponse: MfaResponseTotpCode, mfaResponse: MfaResponseTotpCode
): EmailPasswordAssessment { ): EmailPasswordAssessment {
val response: HttpResponse = RevoltHttp.post("/auth/session/login") { val response: HttpResponse = RevoltHttp.post("/auth/session/login") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
@ -167,7 +166,7 @@ suspend fun authenticateWithMfaTotpCode(
suspend fun authenticateWithMfaRecoveryCode( suspend fun authenticateWithMfaRecoveryCode(
mfaTicket: String, mfaTicket: String,
mfaResponse: MfaResponseRecoveryCode, mfaResponse: MfaResponseRecoveryCode
): EmailPasswordAssessment { ): EmailPasswordAssessment {
val response: HttpResponse = RevoltHttp.post("/auth/session/login") { val response: HttpResponse = RevoltHttp.post("/auth/session/login") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
@ -189,7 +188,6 @@ suspend fun authenticateWithMfaRecoveryCode(
) )
} }
fun friendlySessionName(): String { fun friendlySessionName(): String {
return "Revolt Android on ${Build.MANUFACTURER} ${Build.MODEL}" return "Revolt Android on ${Build.MANUFACTURER} ${Build.MODEL}"
} }

View File

@ -17,7 +17,7 @@ data class RegistrationBody(
val email: String, val email: String,
val password: String, val password: String,
val invite: String? = null, val invite: String? = null,
val captcha: String, val captcha: String
) )
suspend fun register(body: RegistrationBody): RsResult<Unit, RevoltError> { suspend fun register(body: RegistrationBody): RsResult<Unit, RevoltError> {
@ -36,4 +36,4 @@ suspend fun register(body: RegistrationBody): RsResult<Unit, RevoltError> {
} }
return RsResult.ok(Unit) return RsResult.ok(Unit)
} }

View File

@ -70,12 +70,12 @@ data class SendMessageBody(
val content: String, val content: String,
val nonce: String = ULID.makeNext(), val nonce: String = ULID.makeNext(),
val replies: List<SendMessageReply> = emptyList(), val replies: List<SendMessageReply> = emptyList(),
val attachments: List<String>?, val attachments: List<String>?
) )
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class EditMessageBody( data class EditMessageBody(
val content: String?, val content: String?
) )
suspend fun sendMessage( suspend fun sendMessage(
@ -83,7 +83,7 @@ suspend fun sendMessage(
content: String, content: String,
nonce: String? = ULID.makeNext(), nonce: String? = ULID.makeNext(),
replies: List<SendMessageReply>? = null, replies: List<SendMessageReply>? = null,
attachments: List<String>? = null, attachments: List<String>? = null
): String { ): String {
val response = RevoltHttp.post("/channels/$channelId/messages") { val response = RevoltHttp.post("/channels/$channelId/messages") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
@ -101,11 +101,7 @@ suspend fun sendMessage(
return response return response
} }
suspend fun editMessage( suspend fun editMessage(channelId: String, messageId: String, newContent: String? = null) {
channelId: String,
messageId: String,
newContent: String? = null,
) {
val response = RevoltHttp.patch("/channels/$channelId/messages/$messageId") { val response = RevoltHttp.patch("/channels/$channelId/messages/$messageId") {
contentType(ContentType.Application.Json) contentType(ContentType.Application.Json)
setBody( setBody(
@ -146,4 +142,4 @@ suspend fun fetchGroupParticipants(channelId: String): List<User> {
ListSerializer(User.serializer()), ListSerializer(User.serializer()),
response response
) )
} }

View File

@ -12,4 +12,4 @@ suspend fun fetchEmoji(id: String): Emoji {
Emoji.serializer(), Emoji.serializer(),
response response
) )
} }

View File

@ -39,4 +39,4 @@ suspend fun joinInviteByCode(code: String): RsResult<InviteJoined, RevoltError>
val invite = RevoltJson.decodeFromString(InviteJoined.serializer(), response) val invite = RevoltJson.decodeFromString(InviteJoined.serializer(), response)
return RsResult.ok(invite) return RsResult.ok(invite)
} }

View File

@ -35,14 +35,20 @@ suspend fun uploadToAutumn(
val uploadUrl = "$REVOLT_FILES/$tag" val uploadUrl = "$REVOLT_FILES/$tag"
val response = RevoltHttp.post(uploadUrl) { val response = RevoltHttp.post(uploadUrl) {
setBody(MultiPartFormDataContent( setBody(
formData { MultiPartFormDataContent(
append("file", file.readBytes(), Headers.build { formData {
append(HttpHeaders.ContentType, contentType.toString()) append(
append(HttpHeaders.ContentDisposition, "filename=\"$name\"") "file",
}) file.readBytes(),
} Headers.build {
)) append(HttpHeaders.ContentType, contentType.toString())
append(HttpHeaders.ContentDisposition, "filename=\"$name\"")
}
)
}
)
)
onUpload { bytesSentTotal, contentLength -> onUpload { bytesSentTotal, contentLength ->
onProgress(bytesSentTotal, contentLength) onProgress(bytesSentTotal, contentLength)
} }
@ -59,5 +65,4 @@ suspend fun uploadToAutumn(
throw Exception("Unknown error") throw Exception("Unknown error")
} }
} }
}
}

View File

@ -3,5 +3,5 @@ package chat.revolt.api.routes.microservices.january
import chat.revolt.api.REVOLT_JANUARY import chat.revolt.api.REVOLT_JANUARY
fun asJanuaryProxyUrl(url: String): String { fun asJanuaryProxyUrl(url: String): String {
return "$REVOLT_JANUARY/proxy?url=${url}" return "$REVOLT_JANUARY/proxy?url=$url"
} }

View File

@ -51,4 +51,4 @@ data class Voso(
suspend fun getRootRoute(): Root { suspend fun getRootRoute(): Root {
return RevoltHttp.get("/").body() return RevoltHttp.get("/").body()
} }

View File

@ -21,10 +21,7 @@ data class OnboardingResponse(
val onboarding: Boolean val onboarding: Boolean
) )
suspend fun needsOnboarding(sessionToken: String = RevoltAPI.sessionToken): Boolean {
suspend fun needsOnboarding(
sessionToken: String = RevoltAPI.sessionToken,
): Boolean {
val response = RevoltHttp.get("/onboard/hello") { val response = RevoltHttp.get("/onboard/hello") {
header(RevoltAPI.TOKEN_HEADER_NAME, sessionToken) header(RevoltAPI.TOKEN_HEADER_NAME, sessionToken)
} }
@ -36,12 +33,12 @@ suspend fun needsOnboarding(
@Serializable @Serializable
data class OnboardingCompletionBody( data class OnboardingCompletionBody(
val username: String, val username: String
) )
suspend fun completeOnboarding( suspend fun completeOnboarding(
body: OnboardingCompletionBody, body: OnboardingCompletionBody,
sessionToken: String = RevoltAPI.sessionToken, sessionToken: String = RevoltAPI.sessionToken
): RsResult<Unit, RevoltError> { ): RsResult<Unit, RevoltError> {
val response = RevoltHttp.post("/onboard/complete") { val response = RevoltHttp.post("/onboard/complete") {
setBody(body) setBody(body)
@ -67,4 +64,4 @@ suspend fun completeOnboarding(
} }
return RsResult.ok(Unit) return RsResult.ok(Unit)
} }

View File

@ -27,7 +27,7 @@ suspend fun putMessageReport(
report_reason = reason, report_reason = reason,
id = messageId id = messageId
), ),
additional_context = additionalContext, additional_context = additionalContext
) )
val response = RevoltHttp.post("/safety/report") { val response = RevoltHttp.post("/safety/report") {
@ -57,9 +57,9 @@ suspend fun putServerReport(
content = ServerReport( content = ServerReport(
type = "Server", type = "Server",
report_reason = reason, report_reason = reason,
id = serverId, id = serverId
), ),
additional_context = additionalContext, additional_context = additionalContext
) )
val response = RevoltHttp.post("/safety/report") { val response = RevoltHttp.post("/safety/report") {
@ -89,9 +89,9 @@ suspend fun putUserReport(
content = UserReport( content = UserReport(
type = "User", type = "User",
report_reason = reason, report_reason = reason,
id = userId, id = userId
), ),
additional_context = additionalContext, additional_context = additionalContext
) )
val response = RevoltHttp.post("/safety/report") { val response = RevoltHttp.post("/safety/report") {
@ -110,4 +110,4 @@ suspend fun putUserReport(
} catch (e: SerializationException) { } catch (e: SerializationException) {
// Not an error // Not an error
} }
} }

View File

@ -87,4 +87,4 @@ suspend fun leaveOrDeleteServer(serverId: String, leaveSilently: Boolean = false
RevoltHttp.delete("/servers/$serverId") { RevoltHttp.delete("/servers/$serverId") {
parameter("leave_silently", leaveSilently) parameter("leave_silently", leaveSilently)
} }
} }

View File

@ -69,4 +69,4 @@ suspend fun setKey(key: String, value: String) {
) )
) )
} }
} }

View File

@ -15,4 +15,4 @@ suspend fun syncUnreads(): List<ChannelUnreadResponse> {
ListSerializer(ChannelUnreadResponse.serializer()), ListSerializer(ChannelUnreadResponse.serializer()),
response response
) )
} }

View File

@ -7,8 +7,8 @@ import chat.revolt.api.RevoltJson
import io.ktor.client.request.delete import io.ktor.client.request.delete
import io.ktor.client.request.put import io.ktor.client.request.put
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.SerializationException
import kotlin.collections.set import kotlin.collections.set
import kotlinx.serialization.SerializationException
suspend fun blockUser(userId: String) { suspend fun blockUser(userId: String) {
val response = RevoltHttp.put("/users/$userId/block") val response = RevoltHttp.put("/users/$userId/block")
@ -38,4 +38,4 @@ suspend fun unblockUser(userId: String) {
val user = RevoltAPI.userCache[userId] ?: return val user = RevoltAPI.userCache[userId] ?: return
RevoltAPI.userCache[userId] = user.copy(relationship = "None") RevoltAPI.userCache[userId] = user.copy(relationship = "None")
} }

View File

@ -81,4 +81,4 @@ suspend fun fetchUserProfile(id: String): Profile {
} }
return RevoltJson.decodeFromString(Profile.serializer(), response) return RevoltJson.decodeFromString(Profile.serializer(), response)
} }

View File

@ -20,7 +20,7 @@ data class MessagesInChannel(
@Serializable @Serializable
data class ServerUserChoice( data class ServerUserChoice(
val server: String, val server: String,
val user: String, val user: String
) )
@Serializable @Serializable
@ -35,7 +35,7 @@ data class Member(
val roles: List<String>? = null, val roles: List<String>? = null,
val nickname: String? = null, val nickname: String? = null,
val timeout: String? = null, val timeout: String? = null
) { ) {
fun mergeWithPartial(other: Member): Member { fun mergeWithPartial(other: Member): Member {
return Member( return Member(
@ -44,7 +44,7 @@ data class Member(
avatar = other.avatar ?: avatar, avatar = other.avatar ?: avatar,
roles = other.roles ?: roles, roles = other.roles ?: roles,
nickname = other.nickname ?: nickname, nickname = other.nickname ?: nickname,
timeout = other.timeout ?: timeout, timeout = other.timeout ?: timeout
) )
} }
@ -75,7 +75,7 @@ data class Channel(
@SerialName("default_permissions") @SerialName("default_permissions")
val defaultPermissions: PermissionDescription? = null, val defaultPermissions: PermissionDescription? = null,
val nsfw: Boolean? = null, val nsfw: Boolean? = null,
val type: String? = null, // this is _only_ used for websocket events! val type: String? = null // this is _only_ used for websocket events!
) { ) {
fun mergeWithPartial(partial: Channel): Channel { fun mergeWithPartial(partial: Channel): Channel {
return Channel( return Channel(
@ -135,7 +135,7 @@ enum class ChannelType(val value: String) {
@Serializable @Serializable
data class ChannelUserChoice( data class ChannelUserChoice(
val channel: String, val channel: String,
val user: String, val user: String
) )
@Serializable @Serializable
@ -143,7 +143,7 @@ data class ChannelUnreadResponse(
@SerialName("_id") @SerialName("_id")
val id: ChannelUserChoice, val id: ChannelUserChoice,
val last_id: String? = null, val last_id: String? = null,
val mentions: List<String>? = null, val mentions: List<String>? = null
) )
@Serializable @Serializable
@ -151,5 +151,5 @@ data class ChannelUnread(
@SerialName("_id") @SerialName("_id")
val id: String, val id: String,
val last_id: String? = null, val last_id: String? = null,
val mentions: List<String>? = null, val mentions: List<String>? = null
) )

View File

@ -50,5 +50,5 @@ data class AutumnId(
@Serializable @Serializable
data class AutumnError( data class AutumnError(
val type: String, val type: String
) )

View File

@ -43,5 +43,5 @@ data class Invite(
data class InviteJoined( data class InviteJoined(
val type: String? = null, val type: String? = null,
val channels: List<Channel>? = null, val channels: List<Channel>? = null,
val server: Server? = null, val server: Server? = null
) )

View File

@ -21,7 +21,7 @@ data class Message(
val masquerade: Masquerade? = null, val masquerade: Masquerade? = null,
val system: SystemInfo? = null, val system: SystemInfo? = null,
val type: String? = null, // this is _only_ used for websocket events! val type: String? = null, // this is _only_ used for websocket events!
val tail: Boolean? = null, // this is used to determine if the message is the last in a message group val tail: Boolean? = null // this is used to determine if the message is the last in a message group
) { ) {
fun getAuthor(): User? { fun getAuthor(): User? {
return author?.let { RevoltAPI.userCache[it] } return author?.let { RevoltAPI.userCache[it] }
@ -42,7 +42,7 @@ data class Message(
mentions = partial.mentions ?: mentions, mentions = partial.mentions ?: mentions,
masquerade = partial.masquerade ?: masquerade, masquerade = partial.masquerade ?: masquerade,
type = partial.type ?: type, type = partial.type ?: type,
tail = partial.tail ?: tail, tail = partial.tail ?: tail
) )
} }
} }
@ -100,5 +100,5 @@ data class SystemInfo(
val by: String? = null, val by: String? = null,
val from: String? = null, val from: String? = null,
val to: String? = null, val to: String? = null,
val content: String? = null, val content: String? = null
) )

View File

@ -82,37 +82,37 @@ enum class UserReportReason(val value: String) {
data class MessageReport( data class MessageReport(
val type: String, val type: String,
val id: String, val id: String,
val report_reason: ContentReportReason, val report_reason: ContentReportReason
) )
@Serializable @Serializable
data class FullMessageReport( data class FullMessageReport(
val content: MessageReport, val content: MessageReport,
val additional_context: String? = null, val additional_context: String? = null
) )
@Serializable @Serializable
data class ServerReport( data class ServerReport(
val type: String, val type: String,
val id: String, val id: String,
val report_reason: ContentReportReason, val report_reason: ContentReportReason
) )
@Serializable @Serializable
data class FullServerReport( data class FullServerReport(
val content: ServerReport, val content: ServerReport,
val additional_context: String? = null, val additional_context: String? = null
) )
@Serializable @Serializable
data class UserReport( data class UserReport(
val type: String, val type: String,
val id: String, val id: String,
val report_reason: UserReportReason, val report_reason: UserReportReason
) )
@Serializable @Serializable
data class FullUserReport( data class FullUserReport(
val content: UserReport, val content: UserReport,
val additional_context: String? = null, val additional_context: String? = null
) )

View File

@ -19,7 +19,7 @@ data class Server(
val banner: AutumnResource? = null, val banner: AutumnResource? = null,
val flags: Long? = null, val flags: Long? = null,
val analytics: Boolean? = null, val analytics: Boolean? = null,
val discoverable: Boolean? = null, val discoverable: Boolean? = null
) { ) {
fun mergeWithPartial(other: Server): Server { fun mergeWithPartial(other: Server): Server {
return Server( return Server(
@ -36,14 +36,14 @@ data class Server(
banner = other.banner ?: banner, banner = other.banner ?: banner,
flags = other.flags ?: flags, flags = other.flags ?: flags,
analytics = other.analytics ?: analytics, analytics = other.analytics ?: analytics,
discoverable = other.discoverable ?: discoverable, discoverable = other.discoverable ?: discoverable
) )
} }
} }
enum class ServerFlags(val value: Long) { enum class ServerFlags(val value: Long) {
Official(1L shl 0), Official(1L shl 0),
Verified(1L shl 1), Verified(1L shl 1)
} }
infix fun Long?.has(flag: ServerFlags): Boolean { infix fun Long?.has(flag: ServerFlags): Boolean {
@ -90,11 +90,11 @@ data class Emoji(
val name: String? = null, val name: String? = null,
val animated: Boolean? = null, val animated: Boolean? = null,
val nsfw: Boolean? = null, val nsfw: Boolean? = null,
val type: String? = null, // this is _only_ used for websocket events! val type: String? = null // this is _only_ used for websocket events!
) )
@Serializable @Serializable
data class EmojiParent( data class EmojiParent(
val type: String? = null, val type: String? = null,
val id: String? = null val id: String? = null
) )

View File

@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class OrderingSettings( data class OrderingSettings(
val servers: List<String> = emptyList(), val servers: List<String> = emptyList()
) )
@Serializable @Serializable
@ -13,5 +13,5 @@ data class AndroidSpecificSettings(
* The theme to use for the app. * The theme to use for the app.
* Can be one of `{ None, Revolt, Light, M3Dynamic, Amoled }` * Can be one of `{ None, Revolt, Light, M3Dynamic, Amoled }`
*/ */
var theme: String? = null, var theme: String? = null
) )

View File

@ -49,4 +49,4 @@ data class RsResult<V, E>(val value: V?, val error: E?) {
return RsResult(null, error) return RsResult(null, error)
} }
} }
} }

View File

@ -10,7 +10,9 @@ annotation class Treatment(val description: String)
@FeatureFlag("ClosedBetaAccessControl") @FeatureFlag("ClosedBetaAccessControl")
sealed class ClosedBetaAccessControlVariates { sealed class ClosedBetaAccessControlVariates {
@Treatment("Restrict access to the app to users that meet certain or all criteria (implementation-specific)") @Treatment(
"Restrict access to the app to users that meet certain or all criteria (implementation-specific)"
)
data class Restricted(val predicate: () -> Boolean) : ClosedBetaAccessControlVariates() data class Restricted(val predicate: () -> Boolean) : ClosedBetaAccessControlVariates()
@Treatment("Allow access to the app to all users") @Treatment("Allow access to the app to all users")
@ -22,6 +24,6 @@ object FeatureFlags {
var closedBetaAccessControl by mutableStateOf<ClosedBetaAccessControlVariates>( var closedBetaAccessControl by mutableStateOf<ClosedBetaAccessControlVariates>(
ClosedBetaAccessControlVariates.Restricted { ClosedBetaAccessControlVariates.Restricted {
RevoltAPI.channelCache.containsKey("01H7X2KRB0CA4QDSMB4N7WGERF") RevoltAPI.channelCache.containsKey("01H7X2KRB0CA4QDSMB4N7WGERF")
}) }
)
} }

View File

@ -16,4 +16,4 @@ object GlobalState {
fun reset() { fun reset() {
theme = getDefaultTheme() theme = getDefaultTheme()
} }
} }

View File

@ -48,4 +48,4 @@ object SyncedSettings {
_android.value = value _android.value = value
setKey("android", RevoltJson.encodeToString(AndroidSpecificSettings.serializer(), value)) setKey("android", RevoltJson.encodeToString(AndroidSpecificSettings.serializer(), value))
} }
} }

View File

@ -16,13 +16,15 @@ class Unreads {
suspend fun sync() { suspend fun sync() {
channels.clear() channels.clear()
channels.putAll(syncUnreads().associate { channels.putAll(
it.id.channel to ChannelUnread( syncUnreads().associate {
id = it.id.channel, it.id.channel to ChannelUnread(
last_id = it.last_id, id = it.id.channel,
mentions = it.mentions last_id = it.last_id,
) mentions = it.mentions
}) )
}
)
hasLoaded.value = true hasLoaded.value = true
} }
@ -83,4 +85,4 @@ class Unreads {
channels.clear() channels.clear()
hasLoaded.value = false hasLoaded.value = false
} }
} }

View File

@ -11,4 +11,4 @@ sealed class Action {
val ActionChannel = Channel<Action>( val ActionChannel = Channel<Action>(
Channel.BUFFERED Channel.BUFFERED
) )

View File

@ -17,4 +17,4 @@ object UiCallbacks {
suspend fun editMessage(messageId: String) { suspend fun editMessage(messageId: String) {
uiCallbackFlow.emit(UiCallback.EditMessage(messageId)) uiCallbackFlow.emit(UiCallback.EditMessage(messageId))
} }
} }

View File

@ -32,7 +32,7 @@ import chat.revolt.ui.theme.Theme
private val NON_MATERIAL_COLOURS = mapOf( private val NON_MATERIAL_COLOURS = mapOf(
DisconnectionState.Disconnected to (Color(0xff4E0C0C) to Color(0xffff1744)), DisconnectionState.Disconnected to (Color(0xff4E0C0C) to Color(0xffff1744)),
DisconnectionState.Reconnecting to (Color(0xff5B5300) to Color(0xffffea00)), DisconnectionState.Reconnecting to (Color(0xff5B5300) to Color(0xffffea00)),
DisconnectionState.Connected to (Color(0xff0E2F10) to Color(0xff00e676)), DisconnectionState.Connected to (Color(0xff0E2F10) to Color(0xff00e676))
) )
@Composable @Composable
@ -50,7 +50,7 @@ private fun DisconnectedNoticeBase(
.fillMaxWidth() .fillMaxWidth()
.background(background) .background(background)
.padding(vertical = 8.dp, horizontal = 16.dp), .padding(vertical = 8.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
@ -75,10 +75,7 @@ private fun DisconnectedNoticeBase(
} }
@Composable @Composable
fun DisconnectedNotice( fun DisconnectedNotice(state: DisconnectionState, onReconnect: () -> Unit) {
state: DisconnectionState,
onReconnect: () -> Unit
) {
val retries = remember { mutableStateOf(0) } val retries = remember { mutableStateOf(0) }
LaunchedEffect(state) { LaunchedEffect(state) {
@ -101,7 +98,7 @@ fun DisconnectedNotice(
val materialColours = mapOf( val materialColours = mapOf(
DisconnectionState.Disconnected to (MaterialTheme.colorScheme.error to MaterialTheme.colorScheme.onError), DisconnectionState.Disconnected to (MaterialTheme.colorScheme.error to MaterialTheme.colorScheme.onError),
DisconnectionState.Reconnecting to (MaterialTheme.colorScheme.secondary to MaterialTheme.colorScheme.onSecondary), DisconnectionState.Reconnecting to (MaterialTheme.colorScheme.secondary to MaterialTheme.colorScheme.onSecondary),
DisconnectionState.Connected to (MaterialTheme.colorScheme.primary to MaterialTheme.colorScheme.onPrimary), DisconnectionState.Connected to (MaterialTheme.colorScheme.primary to MaterialTheme.colorScheme.onPrimary)
) )
val (background, foreground) = when (GlobalState.theme) { val (background, foreground) = when (GlobalState.theme) {
@ -123,14 +120,14 @@ fun DisconnectedNotice(
background = background, background = background,
foreground = foreground, foreground = foreground,
icon = Icons.Default.Refresh, icon = Icons.Default.Refresh,
text = stringResource(id = R.string.reconnecting), text = stringResource(id = R.string.reconnecting)
) )
DisconnectionState.Connected -> DisconnectedNoticeBase( DisconnectionState.Connected -> DisconnectedNoticeBase(
background = background, background = background,
foreground = foreground, foreground = foreground,
icon = Icons.Default.Done, icon = Icons.Default.Done,
text = stringResource(id = R.string.reconnected), text = stringResource(id = R.string.reconnected)
) )
} }
} }
@ -160,4 +157,4 @@ private fun ReconnectedNoticePreview() {
state = DisconnectionState.Connected, state = DisconnectionState.Connected,
onReconnect = {} onReconnect = {}
) )
} }

View File

@ -28,9 +28,9 @@ import chat.revolt.api.internals.WebCompat
import chat.revolt.api.internals.solidColor import chat.revolt.api.internals.solidColor
import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl
import chat.revolt.api.schemas.Embed import chat.revolt.api.schemas.Embed
import chat.revolt.api.schemas.Embed as EmbedSchema
import chat.revolt.components.generic.RemoteImage import chat.revolt.components.generic.RemoteImage
import chat.revolt.components.generic.UIMarkdown import chat.revolt.components.generic.UIMarkdown
import chat.revolt.api.schemas.Embed as EmbedSchema
@Composable @Composable
fun RegularEmbed( fun RegularEmbed(
@ -66,12 +66,14 @@ fun RegularEmbed(
Row( Row(
modifier = Modifier modifier = Modifier
.then( .then(
if (embed.originalURL != null) if (embed.originalURL != null) {
Modifier Modifier
.clickable { .clickable {
onLinkClick(embed.originalURL) onLinkClick(embed.originalURL)
} }
else Modifier } else {
Modifier
}
) )
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -116,9 +118,13 @@ fun RegularEmbed(
modifier = Modifier modifier = Modifier
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.then( .then(
if (embed.originalURL != null) Modifier.clickable { if (embed.originalURL != null) {
onLinkClick(embed.originalURL) Modifier.clickable {
} else Modifier onLinkClick(embed.originalURL)
}
} else {
Modifier
}
) )
.aspectRatio( .aspectRatio(
(it.width?.toFloat() ?: 0f) / (it.height?.toFloat() ?: 0f) (it.width?.toFloat() ?: 0f) / (it.height?.toFloat() ?: 0f)
@ -130,18 +136,13 @@ fun RegularEmbed(
} }
} }
} }
} }
@Composable @Composable
fun Embed( fun Embed(embed: Embed, modifier: Modifier = Modifier, onLinkClick: (String) -> Unit) {
embed: Embed,
modifier: Modifier = Modifier,
onLinkClick: (String) -> Unit
) {
Column { Column {
when (embed.type) { when (embed.type) {
else -> RegularEmbed(embed = embed, modifier = modifier, onLinkClick = onLinkClick) else -> RegularEmbed(embed = embed, modifier = modifier, onLinkClick = onLinkClick)
} }
} }
} }

View File

@ -33,7 +33,7 @@ fun InReplyTo(
messageId: String, messageId: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
withMention: Boolean = false, withMention: Boolean = false,
onMessageClick: (String) -> Unit = { _ -> }, onMessageClick: (String) -> Unit = { _ -> }
) { ) {
val message = RevoltAPI.messageCache[messageId] val message = RevoltAPI.messageCache[messageId]
val author = RevoltAPI.userCache[message?.author ?: ""] val author = RevoltAPI.userCache[message?.author ?: ""]
@ -116,4 +116,4 @@ fun InReplyTo(
} }
} }
} }
} }

View File

@ -20,7 +20,7 @@ enum class InlineBadge {
fun InlineBadge( fun InlineBadge(
badge: InlineBadge, badge: InlineBadge,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
colour: Color = Color.Unspecified, colour: Color = Color.Unspecified
) { ) {
when (badge) { when (badge) {
InlineBadge.Bot -> Icon( InlineBadge.Bot -> Icon(
@ -62,7 +62,7 @@ fun InlineBadges(
teamMember: Boolean = false, teamMember: Boolean = false,
colour: Color = Color.Unspecified, colour: Color = Color.Unspecified,
precedingIfAny: @Composable () -> Unit = {}, precedingIfAny: @Composable () -> Unit = {},
followingIfAny: @Composable () -> Unit = {}, followingIfAny: @Composable () -> Unit = {}
) { ) {
val hasBadges = bot || bridge || platformModeration || teamMember val hasBadges = bot || bridge || platformModeration || teamMember
@ -104,4 +104,4 @@ fun InlineBadges(
if (hasBadges) { if (hasBadges) {
followingIfAny() followingIfAny()
} }
} }

View File

@ -64,11 +64,11 @@ import chat.revolt.api.internals.WebCompat
import chat.revolt.api.internals.solidColor import chat.revolt.api.internals.solidColor
import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl import chat.revolt.api.routes.microservices.january.asJanuaryProxyUrl
import chat.revolt.api.schemas.AutumnResource import chat.revolt.api.schemas.AutumnResource
import chat.revolt.api.schemas.Message as MessageSchema
import chat.revolt.api.schemas.User import chat.revolt.api.schemas.User
import chat.revolt.components.generic.UserAvatar import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.generic.UserAvatarWidthPlaceholder import chat.revolt.components.generic.UserAvatarWidthPlaceholder
import chat.revolt.internals.markdown.LongClickableSpan import chat.revolt.internals.markdown.LongClickableSpan
import chat.revolt.api.schemas.Message as MessageSchema
@Composable @Composable
fun authorColour(message: MessageSchema): Brush { fun authorColour(message: MessageSchema): Brush {
@ -132,10 +132,7 @@ fun viewAttachmentInBrowser(ctx: android.content.Context, attachment: AutumnReso
viewUrlInBrowser(ctx, url) viewUrlInBrowser(ctx, url)
} }
fun formatLongAsTime(time: Long): String {
fun formatLongAsTime(
time: Long
): String {
val date = java.util.Date(time) val date = java.util.Date(time)
val withinLastWeek = System.currentTimeMillis() - time < 604800000 val withinLastWeek = System.currentTimeMillis() - time < 604800000
@ -167,7 +164,7 @@ fun Message(
onMessageContextMenu: () -> Unit = {}, onMessageContextMenu: () -> Unit = {},
onAvatarClick: () -> Unit = {}, onAvatarClick: () -> Unit = {},
canReply: Boolean = false, canReply: Boolean = false,
onReply: () -> Unit = {}, onReply: () -> Unit = {}
) { ) {
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator() val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
val context = LocalContext.current val context = LocalContext.current
@ -177,7 +174,8 @@ fun Message(
contract = ActivityResultContracts.StartActivityForResult(), contract = ActivityResultContracts.StartActivityForResult(),
onResult = { onResult = {
// do nothing // do nothing
}) }
)
Column { Column {
if (message.tail == false) { if (message.tail == false) {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
@ -237,7 +235,7 @@ fun Message(
userId = author.id ?: message.id ?: ULID.makeSpecial(0), userId = author.id ?: message.id ?: ULID.makeSpecial(0),
avatar = author.avatar, avatar = author.avatar,
rawUrl = authorAvatarUrl(message), rawUrl = authorAvatarUrl(message),
onClick = onAvatarClick, onClick = onAvatarClick
) )
} }
} else { } else {
@ -251,7 +249,7 @@ fun Message(
text = authorName(message), text = authorName(message),
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
brush = authorColour(message), brush = authorColour(message)
), ),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@ -285,7 +283,9 @@ fun Message(
Icon( Icon(
imageVector = Icons.Default.Edit, imageVector = Icons.Default.Edit,
contentDescription = stringResource(id = R.string.edited), contentDescription = stringResource(id = R.string.edited),
tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), tint = MaterialTheme.colorScheme.onBackground.copy(
alpha = 0.5f
),
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
} }
@ -343,7 +343,8 @@ fun Message(
x.toFloat() x.toFloat()
) )
val link = spannedString.getSpans( val link = spannedString.getSpans(
off, off, off,
off,
LongClickableSpan::class.java LongClickableSpan::class.java
) )
if (link.isNotEmpty()) { if (link.isNotEmpty()) {
@ -443,4 +444,4 @@ fun Message(
} }
} }
} }
} }

View File

@ -45,11 +45,11 @@ fun FileAttachment(attachment: AutumnResource) {
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
.padding(8.dp), .padding(8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_file_24dp), painter = painterResource(id = R.drawable.ic_file_24dp),
contentDescription = null, contentDescription = null
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
@ -58,13 +58,13 @@ fun FileAttachment(attachment: AutumnResource) {
Text( Text(
text = attachment.filename ?: "File", text = attachment.filename ?: "File",
maxLines = 1, maxLines = 1,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold
) )
Text( Text(
text = Formatter.formatShortFileSize(context, attachment.size ?: 0), text = Formatter.formatShortFileSize(context, attachment.size ?: 0),
maxLines = 1, maxLines = 1,
color = LocalContentColor.current.copy(alpha = 0.7f), color = LocalContentColor.current.copy(alpha = 0.7f),
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal
) )
} }
} }
@ -80,8 +80,10 @@ fun ImageAttachment(attachment: AutumnResource) {
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(attachment.metadata!!.width!!.toFloat() / attachment.metadata.height!!.toFloat()), .aspectRatio(
description = attachment.filename ?: "Image", attachment.metadata!!.width!!.toFloat() / attachment.metadata.height!!.toFloat()
),
description = attachment.filename ?: "Image"
) )
} }
@ -100,8 +102,10 @@ fun VideoAttachment(attachment: AutumnResource) {
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(attachment.metadata!!.width!!.toFloat() / attachment.metadata.height!!.toFloat()), .aspectRatio(
description = attachment.filename ?: "Video", attachment.metadata!!.width!!.toFloat() / attachment.metadata.height!!.toFloat()
),
description = attachment.filename ?: "Video"
) )
Box( Box(
@ -109,7 +113,7 @@ fun VideoAttachment(attachment: AutumnResource) {
.width(48.dp) .width(48.dp)
.aspectRatio(1f) .aspectRatio(1f)
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)), .background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
) )
Icon( Icon(
@ -117,7 +121,7 @@ fun VideoAttachment(attachment: AutumnResource) {
contentDescription = stringResource(id = R.string.media_viewer_play), contentDescription = stringResource(id = R.string.media_viewer_play),
modifier = Modifier modifier = Modifier
.width(32.dp) .width(32.dp)
.aspectRatio(1f), .aspectRatio(1f)
) )
} }
} }
@ -128,7 +132,7 @@ fun AudioAttachment(attachment: AutumnResource) {
AudioPlayer( AudioPlayer(
url = url, url = url,
filename = attachment.filename ?: "Audio", filename = attachment.filename ?: "Audio",
contentType = attachment.metadata?.type ?: "audio/mpeg", contentType = attachment.metadata?.type ?: "audio/mpeg"
) )
} }
@ -158,4 +162,4 @@ fun MessageAttachment(attachment: AutumnResource, onAttachmentClick: (AutumnReso
else -> FileAttachment(attachment) else -> FileAttachment(attachment)
} }
} }
} }

View File

@ -71,7 +71,7 @@ fun MessageField(
disabled: Boolean = false, disabled: Boolean = false,
editMode: Boolean = false, editMode: Boolean = false,
cancelEdit: () -> Unit = {}, cancelEdit: () -> Unit = {},
onFocusChange: (Boolean) -> Unit = {}, onFocusChange: (Boolean) -> Unit = {}
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val placeholderResource = when (channelType) { val placeholderResource = when (channelType) {
@ -126,7 +126,7 @@ fun MessageField(
channelName channelName
), ),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis
) )
}, },
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
@ -139,7 +139,7 @@ fun MessageField(
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation( unfocusedContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
1.dp 1.dp
), ),
focusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent
), ),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
leadingIcon = { leadingIcon = {
@ -185,7 +185,8 @@ fun MessageField(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
AnimatedVisibility(sendButtonVisible, AnimatedVisibility(
sendButtonVisible,
enter = expandIn(initialSize = { full -> enter = expandIn(initialSize = { full ->
IntSize( IntSize(
0, 0,
@ -203,7 +204,8 @@ fun MessageField(
}) + slideOutHorizontally( }) + slideOutHorizontally(
animationSpec = RevoltTweenInt, animationSpec = RevoltTweenInt,
targetOffsetX = { it } targetOffsetX = { it }
) + fadeOut(animationSpec = RevoltTweenFloat)) { ) + fadeOut(animationSpec = RevoltTweenFloat)
) {
Icon( Icon(
when { when {
editMode -> Icons.Default.Edit editMode -> Icons.Default.Edit
@ -240,4 +242,4 @@ fun MessageFieldPreview() {
channelType = ChannelType.TextChannel, channelType = ChannelType.TextChannel,
channelName = "general" channelName = "general"
) )
} }

View File

@ -13,16 +13,12 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
fun RoleChip( fun RoleChip(label: String, brush: Brush, modifier: Modifier = Modifier) {
label: String,
brush: Brush,
modifier: Modifier = Modifier,
) {
Box( Box(
modifier = modifier modifier = modifier
.clip(MaterialTheme.shapes.small) .clip(MaterialTheme.shapes.small)
.background( .background(
brush = brush, brush = brush
) )
.background( .background(
// darken the background a bit // darken the background a bit
@ -37,4 +33,4 @@ fun RoleChip(
) )
) )
} }
} }

View File

@ -39,13 +39,11 @@ enum class SystemMessageType(val type: String) {
USER_KICKED("user_kicked"), USER_KICKED("user_kicked"),
USER_LEFT("user_left"), USER_LEFT("user_left"),
USER_JOINED("user_joined"), USER_JOINED("user_joined"),
TEXT("text"), TEXT("text")
} }
@Composable @Composable
fun SystemMessage( fun SystemMessage(message: Message) {
message: Message
) {
if (message.system == null) return if (message.system == null) return
val systemMessageType = val systemMessageType =
@ -72,7 +70,9 @@ fun SystemMessage(
when (systemMessageType) { when (systemMessageType) {
SystemMessageType.CHANNEL_OWNERSHIP_CHANGED -> { SystemMessageType.CHANNEL_OWNERSHIP_CHANGED -> {
Text(text = "Channel ownership changed from ${message.system.from} to ${message.system.to}") Text(
text = "Channel ownership changed from ${message.system.from} to ${message.system.to}"
)
} }
SystemMessageType.CHANNEL_ICON_CHANGED -> { SystemMessageType.CHANNEL_ICON_CHANGED -> {
@ -120,11 +120,7 @@ fun SystemMessage(
} }
@Composable @Composable
fun SystemMessageIcon( fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, size: Dp = 24.dp) {
type: SystemMessageType,
modifier: Modifier = Modifier,
size: Dp = 24.dp
) {
when (type) { when (type) {
SystemMessageType.CHANNEL_OWNERSHIP_CHANGED -> { SystemMessageType.CHANNEL_OWNERSHIP_CHANGED -> {
Icon( Icon(
@ -138,7 +134,9 @@ fun SystemMessageIcon(
SystemMessageType.CHANNEL_ICON_CHANGED -> { SystemMessageType.CHANNEL_ICON_CHANGED -> {
Icon( Icon(
painter = painterResource(R.drawable.ic_image_multiple_24dp), painter = painterResource(R.drawable.ic_image_multiple_24dp),
contentDescription = stringResource(R.string.system_message_channel_icon_changed_alt), contentDescription = stringResource(
R.string.system_message_channel_icon_changed_alt
),
tint = LocalContentColor.current, tint = LocalContentColor.current,
modifier = modifier.size(size) modifier = modifier.size(size)
) )
@ -147,7 +145,9 @@ fun SystemMessageIcon(
SystemMessageType.CHANNEL_DESCRIPTION_CHANGED -> { SystemMessageType.CHANNEL_DESCRIPTION_CHANGED -> {
Icon( Icon(
painter = painterResource(R.drawable.ic_text_box_multiple_24dp), painter = painterResource(R.drawable.ic_text_box_multiple_24dp),
contentDescription = stringResource(R.string.system_message_channel_description_changed_alt), contentDescription = stringResource(
R.string.system_message_channel_description_changed_alt
),
tint = LocalContentColor.current, tint = LocalContentColor.current,
modifier = modifier.size(size) modifier = modifier.size(size)
) )
@ -242,4 +242,4 @@ fun SystemMessageIconWithBackground(
) { ) {
SystemMessageIcon(type = type) SystemMessageIcon(type = type)
} }
} }

View File

@ -23,10 +23,7 @@ import androidx.compose.ui.unit.sp
import chat.revolt.R import chat.revolt.R
@Composable @Composable
fun TimeRift( fun TimeRift(modifier: Modifier = Modifier, onMessageLoad: () -> Unit) {
modifier: Modifier = Modifier,
onMessageLoad: () -> Unit,
) {
Column( Column(
modifier = modifier modifier = modifier
.padding(vertical = 10.dp) .padding(vertical = 10.dp)
@ -68,4 +65,4 @@ fun TimeRift(
) )
} }
} }
} }

View File

@ -81,9 +81,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun EmojiPicker( fun EmojiPicker(onEmojiSelected: (String) -> Unit) {
onEmojiSelected: (String) -> Unit,
) {
val view = LocalView.current val view = LocalView.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
@ -104,7 +102,9 @@ fun EmojiPicker(
derivedStateOf { derivedStateOf {
val firstVisible = gridState.firstVisibleItemIndex val firstVisible = gridState.firstVisibleItemIndex
val firstCategory = val firstCategory =
categorySpans.entries.firstOrNull { it.value.first <= firstVisible && it.value.second >= firstVisible }?.key categorySpans.entries.firstOrNull {
it.value.first <= firstVisible && it.value.second >= firstVisible
}?.key
firstCategory firstCategory
} }
@ -234,10 +234,14 @@ fun EmojiPicker(
.padding(4.dp) .padding(4.dp)
.size(24.dp) .size(24.dp)
.then( .then(
if (searchQuery.isNotEmpty()) Modifier.clickable { if (searchQuery.isNotEmpty()) {
searchQuery = "" Modifier.clickable {
focusManager.clearFocus() // this prevents the text field Z-below from gaining focus searchQuery = ""
} else Modifier focusManager.clearFocus() // this prevents the text field Z-below from gaining focus
}
} else {
Modifier
}
) )
.align(Alignment.CenterEnd) .align(Alignment.CenterEnd)
.alpha(clearQueryButtonOpacity.value) .alpha(clearQueryButtonOpacity.value)
@ -259,7 +263,7 @@ fun EmojiPicker(
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
FitzpatrickSkinTone.entries.forEach { skinTone -> FitzpatrickSkinTone.entries.forEach { skinTone ->
Text( Text(
@ -273,12 +277,24 @@ fun EmojiPicker(
.clip(CircleShape) .clip(CircleShape)
.clickable( .clickable(
onClickLabel = when (skinTone) { onClickLabel = when (skinTone) {
FitzpatrickSkinTone.None -> stringResource(R.string.emoji_picker_skin_tone_none) FitzpatrickSkinTone.None -> stringResource(
FitzpatrickSkinTone.Light -> stringResource(R.string.emoji_picker_skin_tone_fitzpatrick_1_2) R.string.emoji_picker_skin_tone_none
FitzpatrickSkinTone.MediumLight -> stringResource(R.string.emoji_picker_skin_tone_fitzpatrick_3) )
FitzpatrickSkinTone.Medium -> stringResource(R.string.emoji_picker_skin_tone_fitzpatrick_4) FitzpatrickSkinTone.Light -> stringResource(
FitzpatrickSkinTone.MediumDark -> stringResource(R.string.emoji_picker_skin_tone_fitzpatrick_5) R.string.emoji_picker_skin_tone_fitzpatrick_1_2
FitzpatrickSkinTone.Dark -> stringResource(R.string.emoji_picker_skin_tone_fitzpatrick_6) )
FitzpatrickSkinTone.MediumLight -> stringResource(
R.string.emoji_picker_skin_tone_fitzpatrick_3
)
FitzpatrickSkinTone.Medium -> stringResource(
R.string.emoji_picker_skin_tone_fitzpatrick_4
)
FitzpatrickSkinTone.MediumDark -> stringResource(
R.string.emoji_picker_skin_tone_fitzpatrick_5
)
FitzpatrickSkinTone.Dark -> stringResource(
R.string.emoji_picker_skin_tone_fitzpatrick_6
)
} }
) { ) {
currentSkinTone = skinTone currentSkinTone = skinTone
@ -286,7 +302,7 @@ fun EmojiPicker(
focusManager.clearFocus() // this prevents the text field Z-below from gaining focus focusManager.clearFocus() // this prevents the text field Z-below from gaining focus
} }
.aspectRatio(1f), .aspectRatio(1f),
textAlign = TextAlign.Center, textAlign = TextAlign.Center
) )
} }
} }
@ -323,7 +339,9 @@ fun EmojiPicker(
} else { } else {
Icons.Default.KeyboardArrowRight Icons.Default.KeyboardArrowRight
}, },
contentDescription = stringResource(R.string.emoji_picker_close_skin_tone_menu), contentDescription = stringResource(
R.string.emoji_picker_close_skin_tone_menu
),
tint = LocalContentColor.current, tint = LocalContentColor.current,
modifier = Modifier modifier = Modifier
.alpha(skinToneMenuCloseHintIconOpacity) .alpha(skinToneMenuCloseHintIconOpacity)
@ -340,7 +358,7 @@ fun EmojiPicker(
.horizontalScroll(categoryRowScrollState) .horizontalScroll(categoryRowScrollState)
.height(37.dp), .height(37.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
servers.forEach { server -> servers.forEach { server ->
Column( Column(
@ -350,13 +368,17 @@ fun EmojiPicker(
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
scope.launch { scope.launch {
val index = val index =
pickerList.indexOfFirst { it is EmojiPickerItem.Section && it.category is Category.ServerEmoteCategory && it.category.server == server } pickerList.indexOfFirst {
it is EmojiPickerItem.Section && it.category is Category.ServerEmoteCategory && it.category.server == server
}
gridState.animateScrollToItem(index) gridState.animateScrollToItem(index)
} }
} }
.then( .then(
if (currentCategory.value is Category.ServerEmoteCategory && (currentCategory.value as Category.ServerEmoteCategory).server == server) { if (currentCategory.value is Category.ServerEmoteCategory && (currentCategory.value as Category.ServerEmoteCategory).server == server) {
Modifier.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)) Modifier.background(
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
} else { } else {
Modifier Modifier
} }
@ -364,7 +386,7 @@ fun EmojiPicker(
.aspectRatio(1f) .aspectRatio(1f)
.padding(4.dp), .padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center
) { ) {
if (server.icon == null) { if (server.icon == null) {
IconPlaceholder( IconPlaceholder(
@ -393,13 +415,17 @@ fun EmojiPicker(
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
scope.launch { scope.launch {
val index = val index =
pickerList.indexOfFirst { it is EmojiPickerItem.Section && it.category is Category.UnicodeEmojiCategory && it.category.definition == category } pickerList.indexOfFirst {
it is EmojiPickerItem.Section && it.category is Category.UnicodeEmojiCategory && it.category.definition == category
}
gridState.animateScrollToItem(index) gridState.animateScrollToItem(index)
} }
} }
.then( .then(
if (currentCategory.value is Category.UnicodeEmojiCategory && (currentCategory.value as Category.UnicodeEmojiCategory).definition == category) { if (currentCategory.value is Category.UnicodeEmojiCategory && (currentCategory.value as Category.UnicodeEmojiCategory).definition == category) {
Modifier.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)) Modifier.background(
MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
} else { } else {
Modifier Modifier
} }
@ -407,24 +433,44 @@ fun EmojiPicker(
.aspectRatio(1f) .aspectRatio(1f)
.padding(4.dp), .padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center
) { ) {
Icon( Icon(
painter = when (category) { painter = when (category) {
UnicodeEmojiSection.Smileys -> painterResource(R.drawable.ic_emoticon_24dp) UnicodeEmojiSection.Smileys -> painterResource(
UnicodeEmojiSection.People -> painterResource(R.drawable.ic_human_greeting_variant_24dp) R.drawable.ic_emoticon_24dp
UnicodeEmojiSection.Animals -> painterResource(R.drawable.ic_snake_24dp) )
UnicodeEmojiSection.Food -> painterResource(R.drawable.ic_glass_mug_variant_24dp) UnicodeEmojiSection.People -> painterResource(
UnicodeEmojiSection.Travel -> painterResource(R.drawable.ic_train_bus_24dp) R.drawable.ic_human_greeting_variant_24dp
UnicodeEmojiSection.Activities -> painterResource(R.drawable.ic_skate_24dp) )
UnicodeEmojiSection.Objects -> painterResource(R.drawable.ic_table_chair_24dp) UnicodeEmojiSection.Animals -> painterResource(
UnicodeEmojiSection.Symbols -> painterResource(R.drawable.ic_symbol_24dp) R.drawable.ic_snake_24dp
UnicodeEmojiSection.Flags -> painterResource(R.drawable.ic_flag_24dp) )
UnicodeEmojiSection.Food -> painterResource(
R.drawable.ic_glass_mug_variant_24dp
)
UnicodeEmojiSection.Travel -> painterResource(
R.drawable.ic_train_bus_24dp
)
UnicodeEmojiSection.Activities -> painterResource(
R.drawable.ic_skate_24dp
)
UnicodeEmojiSection.Objects -> painterResource(
R.drawable.ic_table_chair_24dp
)
UnicodeEmojiSection.Symbols -> painterResource(
R.drawable.ic_symbol_24dp
)
UnicodeEmojiSection.Flags -> painterResource(
R.drawable.ic_flag_24dp
)
}, },
contentDescription = null, contentDescription = null,
tint = if (currentCategory.value is Category.UnicodeEmojiCategory && (currentCategory.value as Category.UnicodeEmojiCategory).definition == category) { tint = if (currentCategory.value is Category.UnicodeEmojiCategory && (currentCategory.value as Category.UnicodeEmojiCategory).definition == category) {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
} else LocalContentColor.current } else {
LocalContentColor.current
}
) )
} }
} }
@ -443,7 +489,7 @@ fun EmojiPicker(
key = "searchResultsHeader", key = "searchResultsHeader",
span = { span = {
GridItemSpan(spanCount) GridItemSpan(spanCount)
}, }
) { ) {
Text( Text(
text = stringResource(R.string.emoji_picker_search_results_header), text = stringResource(R.string.emoji_picker_search_results_header),
@ -481,7 +527,7 @@ fun EmojiPicker(
key = "searchResultsFooter", key = "searchResultsFooter",
span = { span = {
GridItemSpan(spanCount) GridItemSpan(spanCount)
}, }
) { ) {
Divider() Divider()
} }
@ -516,7 +562,7 @@ fun ColumnScope.PickerItem(
skinToneFactory: (EmojiPickerItem.UnicodeEmoji) -> String, skinToneFactory: (EmojiPickerItem.UnicodeEmoji) -> String,
onClick: (EmojiPickerItem) -> Unit, onClick: (EmojiPickerItem) -> Unit,
onServerEmoteInfo: (String) -> Unit, onServerEmoteInfo: (String) -> Unit,
lesserHeaders: Boolean = false, lesserHeaders: Boolean = false
) { ) {
when (item) { when (item) {
is EmojiPickerItem.UnicodeEmoji -> { is EmojiPickerItem.UnicodeEmoji -> {
@ -528,7 +574,7 @@ fun ColumnScope.PickerItem(
.aspectRatio(1f) .aspectRatio(1f)
.weight(1f), .weight(1f),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = skinToneFactory(item), text = skinToneFactory(item),
@ -548,7 +594,7 @@ fun ColumnScope.PickerItem(
.aspectRatio(1f) .aspectRatio(1f)
.weight(1f), .weight(1f),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center
) { ) {
RemoteImage( RemoteImage(
url = "$REVOLT_FILES/emojis/${item.emote.id}/emoji.gif", url = "$REVOLT_FILES/emojis/${item.emote.id}/emoji.gif",
@ -564,9 +610,12 @@ fun ColumnScope.PickerItem(
is EmojiPickerItem.Section -> { is EmojiPickerItem.Section -> {
Text( Text(
when (item.category) { when (item.category) {
is Category.UnicodeEmojiCategory -> stringResource(item.category.definition.nameResource) is Category.UnicodeEmojiCategory -> stringResource(
is Category.ServerEmoteCategory -> item.category.server.name item.category.definition.nameResource
?: stringResource(R.string.unknown) )
is Category.ServerEmoteCategory ->
item.category.server.name
?: stringResource(R.string.unknown)
}, },
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
modifier = Modifier modifier = Modifier

View File

@ -31,7 +31,7 @@ import androidx.compose.ui.unit.dp
fun CollapsibleCard( fun CollapsibleCard(
header: @Composable () -> Unit, header: @Composable () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable () -> Unit, content: @Composable () -> Unit
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
@ -45,7 +45,7 @@ fun CollapsibleCard(
modifier = Modifier modifier = Modifier
.clickable { expanded = !expanded } .clickable { expanded = !expanded }
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(16.dp)
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalTextStyle provides MaterialTheme.typography.titleMedium.copy( LocalTextStyle provides MaterialTheme.typography.titleMedium.copy(
@ -87,4 +87,4 @@ fun CollapsibleCardPreview() {
) )
} }
) )
} }

View File

@ -21,7 +21,7 @@ fun FormTextField(
type: KeyboardType = KeyboardType.Text, type: KeyboardType = KeyboardType.Text,
supportingText: @Composable (() -> Unit)? = null, supportingText: @Composable (() -> Unit)? = null,
singleLine: Boolean = true, singleLine: Boolean = true,
enabled: Boolean = true, enabled: Boolean = true
) { ) {
TextField( TextField(
value = value, value = value,
@ -34,4 +34,4 @@ fun FormTextField(
enabled = enabled, enabled = enabled,
modifier = modifier modifier = modifier
) )
} }

View File

@ -31,11 +31,14 @@ fun IconPlaceholder(
modifier = modifier modifier = modifier
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
.then( .then(
if (onClick != NoopHandler || onLongClick != NoopHandler) Modifier.combinedClickable( if (onClick != NoopHandler || onLongClick != NoopHandler) {
onClick = onClick, Modifier.combinedClickable(
onLongClick = onLongClick onClick = onClick,
) onLongClick = onLongClick
else Modifier )
} else {
Modifier
}
) )
) { ) {
Text( Text(
@ -45,4 +48,4 @@ fun IconPlaceholder(
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
} }
} }

View File

@ -46,7 +46,7 @@ fun UIMarkdown(
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
fontSize: TextUnit = LocalTextStyle.current.fontSize, fontSize: TextUnit = LocalTextStyle.current.fontSize,
maxLines: Int = Int.MAX_VALUE, maxLines: Int = Int.MAX_VALUE
) { ) {
val context = LocalContext.current val context = LocalContext.current
val foregroundColor = LocalContentColor.current val foregroundColor = LocalContentColor.current
@ -61,7 +61,7 @@ fun UIMarkdown(
.addRevoltRules(context) .addRevoltRules(context)
.addRules( .addRules(
createCodeRule(context, codeBlockColor.toArgb()), createCodeRule(context, codeBlockColor.toArgb()),
createInlineCodeRule(context, codeBlockColor.toArgb()), createInlineCodeRule(context, codeBlockColor.toArgb())
) )
.addRules( .addRules(
SimpleMarkdownRules.createSimpleMarkdownRules( SimpleMarkdownRules.createSimpleMarkdownRules(
@ -109,7 +109,7 @@ fun UIMarkdown(
modifier = modifier, modifier = modifier,
update = { update = {
it.text = spannableStringBuilder.value it.text = spannableStringBuilder.value
}, }
) )
} }
@ -118,6 +118,6 @@ fun UIMarkdown(
fun UIMarkdownPreview() { fun UIMarkdownPreview() {
UIMarkdown( UIMarkdown(
text = "Hello, **world**! <@01F1WKM5TK2V6KCZWR6DGBJDTZ> [link](https://google.com) `code`\n\n```kt\nfun main() {\n println(\"Hello, world!\")\n}\n```", text = "Hello, **world**! <@01F1WKM5TK2V6KCZWR6DGBJDTZ> [link](https://google.com) `code`\n\n```kt\nfun main() {\n println(\"Hello, world!\")\n}\n```",
fontSize = 16.sp, fontSize = 16.sp
) )
} }

View File

@ -72,7 +72,7 @@ fun NonIdealState(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
) { ) {
actions() actions()
} }
@ -113,7 +113,7 @@ fun NonIdealStatePreviewNoActions() {
) )
}, },
title = { Text("Error") }, title = { Text("Error") },
description = { Text("Could not load channels.") }, description = { Text("Could not load channels.") }
) )
} }
@ -128,6 +128,6 @@ fun NonIdealStatePreviewNoDescription() {
modifier = Modifier.size(it) modifier = Modifier.size(it)
) )
}, },
title = { Text("No channels") }, title = { Text("No channels") }
) )
} }

View File

@ -28,7 +28,7 @@ fun PageHeader(
showBackButton: Boolean = false, showBackButton: Boolean = false,
onBackButtonClicked: () -> Unit = {}, onBackButtonClicked: () -> Unit = {},
additionalButtons: @Composable () -> Unit = {}, additionalButtons: @Composable () -> Unit = {},
maxLines: Int = Int.MAX_VALUE, maxLines: Int = Int.MAX_VALUE
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -53,7 +53,7 @@ fun PageHeader(
), ),
modifier = modifier modifier = modifier
.padding(horizontal = 15.dp, vertical = 15.dp) .padding(horizontal = 15.dp, vertical = 15.dp)
.weight(1f), .weight(1f)
) )
additionalButtons() additionalButtons()
} }
@ -83,4 +83,4 @@ fun PageHeaderPreviewWithAdditionalButtons() {
) )
} }
}) })
} }

View File

@ -65,4 +65,4 @@ fun PrimaryTabsPreview() {
currentIndex = 0, currentIndex = 0,
onTabSelected = {} onTabSelected = {}
) )
} }

View File

@ -21,13 +21,17 @@ fun RemoteImage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Crop, contentScale: ContentScale = ContentScale.Crop,
width: Int = 0, width: Int = 0,
height: Int = 0, height: Int = 0
) { ) {
val context = LocalContext.current val context = LocalContext.current
fun pxAsDp(px: Int): Dp { fun pxAsDp(px: Int): Dp {
return (px / (context.resources return (
.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).dp px / (
context.resources
.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
)
).dp
} }
GlideImage( GlideImage(
@ -39,4 +43,4 @@ fun RemoteImage(
.height(pxAsDp(height)), .height(pxAsDp(height)),
transition = CrossFade transition = CrossFade
) )
} }

View File

@ -27,9 +27,11 @@ fun SheetClickable(
label: @Composable (TextStyle) -> Unit, label: @Composable (TextStyle) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
dangerous: Boolean = false, dangerous: Boolean = false,
onClick: () -> Unit, onClick: () -> Unit
) { ) {
CompositionLocalProvider(LocalContentColor provides if (dangerous) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground) { CompositionLocalProvider(
LocalContentColor provides if (dangerous) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground
) {
Box(modifier = modifier.padding(vertical = 4.dp)) { Box(modifier = modifier.padding(vertical = 4.dp)) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -44,7 +46,7 @@ fun SheetClickable(
label( label(
MaterialTheme.typography.bodyMedium.copy( MaterialTheme.typography.bodyMedium.copy(
color = LocalContentColor.current, color = LocalContentColor.current,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold
) )
) )
} }
@ -68,4 +70,4 @@ fun SettingsCategoryPreview() {
}, },
onClick = {} onClick = {}
) )
} }

View File

@ -78,7 +78,7 @@ fun UserAvatar(
size: Dp = 40.dp, size: Dp = 40.dp,
presenceSize: Dp = 16.dp, presenceSize: Dp = 16.dp,
onLongClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null
) { ) {
Box( Box(
modifier = modifier modifier = modifier
@ -94,29 +94,35 @@ fun UserAvatar(
.clip(CircleShape) .clip(CircleShape)
.size(size) .size(size)
.then( .then(
if (onLongClick != null || onClick != null) Modifier if (onLongClick != null || onClick != null) {
.combinedClickable( Modifier
onClick = { onClick?.invoke() }, .combinedClickable(
onLongClick = { onLongClick?.invoke() } onClick = { onClick?.invoke() },
) onLongClick = { onLongClick?.invoke() }
else Modifier )
} else {
Modifier
}
) )
) )
} else { } else {
RemoteImage( RemoteImage(
url = "$REVOLT_BASE/users/${userId}/default_avatar", url = "$REVOLT_BASE/users/$userId/default_avatar",
description = stringResource(id = R.string.avatar_alt, username), description = stringResource(id = R.string.avatar_alt, username),
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.size(size) .size(size)
.then( .then(
if (onLongClick != null || onClick != null) Modifier if (onLongClick != null || onClick != null) {
.combinedClickable( Modifier
onClick = { onClick?.invoke() }, .combinedClickable(
onLongClick = { onLongClick?.invoke() } onClick = { onClick?.invoke() },
) onLongClick = { onLongClick?.invoke() }
else Modifier )
), } else {
Modifier
}
)
) )
} }
@ -135,7 +141,7 @@ fun GroupIcon(
rawUrl: String? = null, rawUrl: String? = null,
size: Dp = 40.dp, size: Dp = 40.dp,
onLongClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null
) { ) {
Box( Box(
modifier = modifier modifier = modifier
@ -151,12 +157,15 @@ fun GroupIcon(
.clip(MaterialTheme.shapes.small) .clip(MaterialTheme.shapes.small)
.size(size) .size(size)
.then( .then(
if (onLongClick != null || onClick != null) Modifier if (onLongClick != null || onClick != null) {
.combinedClickable( Modifier
onClick = { onClick?.invoke() }, .combinedClickable(
onLongClick = { onLongClick?.invoke() } onClick = { onClick?.invoke() },
) onLongClick = { onLongClick?.invoke() }
else Modifier )
} else {
Modifier
}
) )
) )
} else { } else {
@ -164,12 +173,15 @@ fun GroupIcon(
modifier = Modifier modifier = Modifier
.size(size) .size(size)
.then( .then(
if (onLongClick != null || onClick != null) Modifier if (onLongClick != null || onClick != null) {
.combinedClickable( Modifier
onClick = { onClick?.invoke() }, .combinedClickable(
onLongClick = { onLongClick?.invoke() } onClick = { onClick?.invoke() },
) onLongClick = { onLongClick?.invoke() }
else Modifier )
} else {
Modifier
}
) )
.clip(MaterialTheme.shapes.small) .clip(MaterialTheme.shapes.small)
.background(MaterialTheme.colorScheme.primary) .background(MaterialTheme.colorScheme.primary)
@ -186,9 +198,7 @@ fun GroupIcon(
} }
@Composable @Composable
fun UserAvatarWidthPlaceholder( fun UserAvatarWidthPlaceholder(size: Dp = 40.dp) {
size: Dp = 40.dp,
) {
Box( Box(
modifier = Modifier modifier = Modifier
.width(size) .width(size)
@ -205,4 +215,4 @@ fun UserAvatarWithPresencePreview() {
userId = "01F1WKM5TK2V6KCZWR6DGBJDTZ", userId = "01F1WKM5TK2V6KCZWR6DGBJDTZ",
presence = Presence.Online presence = Presence.Online
) )
} }

View File

@ -50,7 +50,7 @@ fun WebMarkdown(
text: String, text: String,
maskLoading: Boolean = false, maskLoading: Boolean = false,
simpleLineBreaks: Boolean = true, simpleLineBreaks: Boolean = true,
modifier: Modifier = Modifier, modifier: Modifier = Modifier
) { ) {
val contentColour = LocalContentColor.current val contentColour = LocalContentColor.current
val materialColourScheme = MaterialTheme.colorScheme val materialColourScheme = MaterialTheme.colorScheme
@ -97,9 +97,11 @@ fun WebMarkdown(
): Boolean { ): Boolean {
// Capture clicks on invite links // Capture clicks on invite links
if (webResourceRequest.url.host == "rvlt.gg" || if (webResourceRequest.url.host == "rvlt.gg" ||
(webResourceRequest.url.host?.endsWith("revolt.chat") == true && webResourceRequest.url.path?.startsWith( (
"/invite" webResourceRequest.url.host?.endsWith("revolt.chat") == true && webResourceRequest.url.path?.startsWith(
) == true) "/invite"
) == true
)
) { ) {
val intent = Intent( val intent = Intent(
context, context,
@ -129,7 +131,7 @@ fun WebMarkdown(
} }
loadUrl( loadUrl(
"$REVOLT_APP/_android_assets/webmarkdown/renderer.html", "$REVOLT_APP/_android_assets/webmarkdown/renderer.html"
) )
settings.apply { settings.apply {
@ -185,4 +187,4 @@ fun WebMarkdown(
it.evaluateJavascript("renderMarkdown()", null) it.evaluateJavascript("renderMarkdown()", null)
} }
) )
} }

View File

@ -66,4 +66,4 @@ fun AnyLinkPreview() {
AnyLink(text = "Click me! #${clicked.value}", action = { AnyLink(text = "Click me! #${clicked.value}", action = {
clicked.value++ clicked.value++
}) })
} }

View File

@ -51,11 +51,7 @@ import kotlinx.coroutines.launch
@Composable @Composable
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun AudioPlayer( fun AudioPlayer(url: String, filename: String, contentType: String) {
url: String,
filename: String,
contentType: String,
) {
val context = LocalContext.current val context = LocalContext.current
val showMenu = remember { mutableStateOf(false) } val showMenu = remember { mutableStateOf(false) }
@ -201,7 +197,7 @@ fun AudioPlayer(
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp), modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp)
) { ) {
Text( Text(
text = filename, text = filename,
@ -222,7 +218,7 @@ fun AudioPlayer(
} }
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton(onClick = { IconButton(onClick = {
if (isPlaying.value) { if (isPlaying.value) {
@ -237,12 +233,12 @@ fun AudioPlayer(
if (isPlaying.value) { if (isPlaying.value) {
Icon( Icon(
painter = painterResource(R.drawable.ic_pause_24dp), painter = painterResource(R.drawable.ic_pause_24dp),
contentDescription = stringResource(R.string.media_viewer_pause), contentDescription = stringResource(R.string.media_viewer_pause)
) )
} else { } else {
Icon( Icon(
imageVector = Icons.Filled.PlayArrow, imageVector = Icons.Filled.PlayArrow,
contentDescription = stringResource(R.string.media_viewer_play), contentDescription = stringResource(R.string.media_viewer_play)
) )
} }
} }
@ -270,7 +266,7 @@ fun AudioPlayer(
}) { }) {
Icon( Icon(
imageVector = Icons.Filled.MoreVert, imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(R.string.media_viewer_more), contentDescription = stringResource(R.string.media_viewer_more)
) )
DropdownMenu( DropdownMenu(
expanded = showMenu.value, expanded = showMenu.value,
@ -298,4 +294,4 @@ fun AudioPlayer(
} }
} }
} }
} }

View File

@ -86,7 +86,7 @@ private fun Long.formatAsLengthDuration(): String {
if (days > 0) "$days:" else null, if (days > 0) "$days:" else null,
if (hours > 0) "$hours".padStart(2, '0') + ":" else null, if (hours > 0) "$hours".padStart(2, '0') + ":" else null,
"$minutes".padStart(2, '0') + ":", "$minutes".padStart(2, '0') + ":",
"$seconds".padStart(2, '0'), "$seconds".padStart(2, '0')
) )
} }
@ -102,7 +102,7 @@ fun InbuiltMediaPicker(
onClose: () -> Unit, onClose: () -> Unit,
onMediaSelected: (Media) -> Unit, onMediaSelected: (Media) -> Unit,
pendingMedia: List<String>, pendingMedia: List<String>,
disabled: Boolean = false, disabled: Boolean = false
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -140,14 +140,16 @@ fun InbuiltMediaPicker(
onResult = { mediaPermissionState -> onResult = { mediaPermissionState ->
hasPhotoPermission = mediaPermissionState[Manifest.permission.READ_MEDIA_IMAGES] == true hasPhotoPermission = mediaPermissionState[Manifest.permission.READ_MEDIA_IMAGES] == true
hasVideoPermission = mediaPermissionState[Manifest.permission.READ_MEDIA_VIDEO] == true hasVideoPermission = mediaPermissionState[Manifest.permission.READ_MEDIA_VIDEO] == true
mediaPermissionIsPartial = if (!hasMediaPermissions mediaPermissionIsPartial = if (!hasMediaPermissions &&
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
&& mediaPermissionState[Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED] == true mediaPermissionState[Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED] == true
) { ) {
true true
} else if (hasMediaPermissions) { } else if (hasMediaPermissions) {
false false
} else null } else {
null
}
} }
) )
@ -158,9 +160,9 @@ fun InbuiltMediaPicker(
} }
LaunchedEffect(hasMediaPermissions) { LaunchedEffect(hasMediaPermissions) {
mediaPermissionIsPartial = if (!hasMediaPermissions mediaPermissionIsPartial = if (!hasMediaPermissions &&
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
&& ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
context, context,
Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
) == PermissionChecker.PERMISSION_GRANTED ) == PermissionChecker.PERMISSION_GRANTED
@ -168,7 +170,9 @@ fun InbuiltMediaPicker(
true true
} else if (hasMediaPermissions) { } else if (hasMediaPermissions) {
false false
} else null } else {
null
}
} }
LaunchedEffect(hasMediaPermissions, mediaPermissionIsPartial) { LaunchedEffect(hasMediaPermissions, mediaPermissionIsPartial) {
@ -178,7 +182,7 @@ fun InbuiltMediaPicker(
MediaStore.Images.ImageColumns.RESOLUTION, MediaStore.Images.ImageColumns.RESOLUTION,
MediaStore.Images.ImageColumns.ORIENTATION, MediaStore.Images.ImageColumns.ORIENTATION,
MediaStore.Images.ImageColumns.MIME_TYPE, MediaStore.Images.ImageColumns.MIME_TYPE,
MediaStore.Video.VideoColumns.DURATION, MediaStore.Video.VideoColumns.DURATION
) )
val selection: String? = null val selection: String? = null
@ -199,16 +203,30 @@ fun InbuiltMediaPicker(
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
try { try {
val id = val id =
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID)) cursor.getLong(
cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID)
)
val resolution = val resolution =
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.RESOLUTION)) cursor.getString(
cursor.getColumnIndexOrThrow(
MediaStore.Images.ImageColumns.RESOLUTION
)
)
val orientation = val orientation =
cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.ORIENTATION)) cursor.getInt(
cursor.getColumnIndexOrThrow(
MediaStore.Images.ImageColumns.ORIENTATION
)
)
val swapDimensions = orientation == 90 || orientation == 270 val swapDimensions = orientation == 90 || orientation == 270
val isVideo = val isVideo =
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE)) cursor.getString(
cursor.getColumnIndexOrThrow(
MediaStore.Images.ImageColumns.MIME_TYPE
)
)
.startsWith("video") .startsWith("video")
val durationColumn = val durationColumn =
@ -293,7 +311,7 @@ fun InbuiltMediaPicker(
alpha = 0.5f alpha = 0.5f
) )
), ),
textAlign = TextAlign.Center, textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -311,12 +329,14 @@ fun InbuiltMediaPicker(
permissionRequester.launch( permissionRequester.launch(
arrayOf( arrayOf(
Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.READ_MEDIA_VIDEO
) )
) )
} }
}) { }) {
Text(text = stringResource(id = R.string.file_picker_permission_request_cta)) Text(
text = stringResource(id = R.string.file_picker_permission_request_cta)
)
} }
} }
} else { } else {
@ -324,7 +344,7 @@ fun InbuiltMediaPicker(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .fillMaxHeight()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp)
) { ) {
AnimatedVisibility( AnimatedVisibility(
mediaPermissionIsPartial == true mediaPermissionIsPartial == true
@ -343,7 +363,9 @@ fun InbuiltMediaPicker(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Image( Image(
painter = painterResource(id = R.drawable.ux_file_unpartialise_request), painter = painterResource(
id = R.drawable.ux_file_unpartialise_request
),
modifier = Modifier modifier = Modifier
.size(52.dp), .size(52.dp),
contentDescription = null // decorative contentDescription = null // decorative
@ -354,14 +376,18 @@ fun InbuiltMediaPicker(
.weight(1f) .weight(1f)
) { ) {
Text( Text(
text = stringResource(id = R.string.file_picker_permission_unpartialise_request_header), text = stringResource(
id = R.string.file_picker_permission_unpartialise_request_header
),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = stringResource(id = R.string.file_picker_permission_unpartialise_request_body), text = stringResource(
id = R.string.file_picker_permission_unpartialise_request_body
),
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodyMedium.copy(
color = LocalContentColor.current.copy( color = LocalContentColor.current.copy(
alpha = 0.5f alpha = 0.5f
@ -384,12 +410,16 @@ fun InbuiltMediaPicker(
permissionRequester.launch( permissionRequester.launch(
arrayOf( arrayOf(
Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO, Manifest.permission.READ_MEDIA_VIDEO
) )
) )
} }
}) { }) {
Text(text = stringResource(id = R.string.file_picker_permission_unpartialise_request_cta)) Text(
text = stringResource(
id = R.string.file_picker_permission_unpartialise_request_cta
)
)
} }
} }
} }
@ -404,7 +434,9 @@ fun InbuiltMediaPicker(
onOpenDocumentsUi() onOpenDocumentsUi()
}, },
label = { label = {
Text(text = stringResource(id = R.string.file_picker_chip_documents)) Text(
text = stringResource(id = R.string.file_picker_chip_documents)
)
}, },
leadingIcon = { leadingIcon = {
Icon( Icon(
@ -468,14 +500,14 @@ fun InbuiltMediaPicker(
} }
) )
.width(100.dp) .width(100.dp)
.aspectRatio(images[image].aspectRatio), .aspectRatio(images[image].aspectRatio)
) { ) {
GlideImage( GlideImage(
model = images[image].uri.toString(), model = images[image].uri.toString(),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.fillMaxSize(), .fillMaxSize()
) )
if (images[image].duration != null) { if (images[image].duration != null) {
@ -502,4 +534,4 @@ fun InbuiltMediaPicker(
} }
} }
} }
} }

View File

@ -36,7 +36,7 @@ fun AttachmentManager(
attachments: List<FileArgs>, attachments: List<FileArgs>,
uploading: Boolean, uploading: Boolean,
uploadProgress: Float = 0f, uploadProgress: Float = 0f,
onRemove: (FileArgs) -> Unit, onRemove: (FileArgs) -> Unit
) { ) {
val animatedProgress by animateFloatAsState( val animatedProgress by animateFloatAsState(
targetValue = uploadProgress, targetValue = uploadProgress,
@ -54,7 +54,6 @@ fun AttachmentManager(
.horizontalScroll(rememberScrollState()) .horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp, vertical = 4.dp) .padding(horizontal = 8.dp, vertical = 4.dp)
) { ) {
attachments.forEach { attachment -> attachments.forEach { attachment ->
Row( Row(
modifier = Modifier modifier = Modifier
@ -98,20 +97,20 @@ fun AttachmentManagerPreview() {
FileArgs( FileArgs(
filename = "file1.png", filename = "file1.png",
contentType = "image/png", contentType = "image/png",
file = File("file1.png"), file = File("file1.png")
), ),
FileArgs( FileArgs(
filename = "file2.png", filename = "file2.png",
contentType = "image/png", contentType = "image/png",
file = File("file2.png"), file = File("file2.png")
), ),
FileArgs( FileArgs(
filename = "file3.png", filename = "file3.png",
contentType = "image/png", contentType = "image/png",
file = File("file3.png"), file = File("file3.png")
), )
), ),
uploading = false, uploading = false,
onRemove = {} onRemove = {}
) )
} }

View File

@ -32,7 +32,7 @@ fun ChannelHeader(
channel: Channel, channel: Channel,
onChannelClick: (String) -> Unit, onChannelClick: (String) -> Unit,
onToggleDrawer: () -> Unit, onToggleDrawer: () -> Unit,
useDrawer: Boolean, useDrawer: Boolean
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -41,7 +41,7 @@ fun ChannelHeader(
channel.id?.let { onChannelClick(it) } channel.id?.let { onChannelClick(it) }
} }
.padding(vertical = 4.dp, horizontal = 4.dp), .padding(vertical = 4.dp, horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
if (useDrawer) { if (useDrawer) {
IconButton(onClick = { IconButton(onClick = {
@ -49,7 +49,7 @@ fun ChannelHeader(
}) { }) {
Icon( Icon(
imageVector = Icons.Default.Menu, imageVector = Icons.Default.Menu,
contentDescription = stringResource(R.string.menu), contentDescription = stringResource(R.string.menu)
) )
} }
@ -74,7 +74,7 @@ fun ChannelHeader(
Row( Row(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = channel.name text = channel.name
@ -86,7 +86,7 @@ fun ChannelHeader(
}, },
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
@ -96,8 +96,8 @@ fun ChannelHeader(
contentDescription = stringResource(R.string.menu), contentDescription = stringResource(R.string.menu),
modifier = Modifier modifier = Modifier
.size(18.dp) .size(18.dp)
.alpha(0.4f), .alpha(0.4f)
) )
} }
} }
} }

View File

@ -15,44 +15,41 @@ import chat.revolt.R
import chat.revolt.api.schemas.ChannelType import chat.revolt.api.schemas.ChannelType
@Composable @Composable
fun ChannelIcon( fun ChannelIcon(channelType: ChannelType, modifier: Modifier = Modifier) {
channelType: ChannelType,
modifier: Modifier = Modifier,
) {
when (channelType) { when (channelType) {
ChannelType.TextChannel -> { ChannelType.TextChannel -> {
Icon( Icon(
painter = painterResource(R.drawable.ic_pound_24dp), painter = painterResource(R.drawable.ic_pound_24dp),
contentDescription = stringResource(R.string.channel_text), contentDescription = stringResource(R.string.channel_text),
modifier = modifier, modifier = modifier
) )
} }
ChannelType.VoiceChannel -> { ChannelType.VoiceChannel -> {
Icon( Icon(
painter = painterResource(R.drawable.ic_volume_up_24dp), painter = painterResource(R.drawable.ic_volume_up_24dp),
contentDescription = stringResource(R.string.channel_voice), contentDescription = stringResource(R.string.channel_voice),
modifier = modifier, modifier = modifier
) )
} }
ChannelType.SavedMessages -> { ChannelType.SavedMessages -> {
Icon( Icon(
painter = painterResource(R.drawable.ic_note_24dp), painter = painterResource(R.drawable.ic_note_24dp),
contentDescription = stringResource(R.string.channel_notes), contentDescription = stringResource(R.string.channel_notes),
modifier = modifier, modifier = modifier
) )
} }
ChannelType.DirectMessage -> { ChannelType.DirectMessage -> {
Icon( Icon(
imageVector = Icons.Default.AccountCircle, imageVector = Icons.Default.AccountCircle,
contentDescription = stringResource(R.string.channel_dm), contentDescription = stringResource(R.string.channel_dm),
modifier = modifier, modifier = modifier
) )
} }
ChannelType.Group -> { ChannelType.Group -> {
Icon( Icon(
imageVector = Icons.Default.AccountBox, imageVector = Icons.Default.AccountBox,
contentDescription = stringResource(R.string.channel_group), contentDescription = stringResource(R.string.channel_group),
modifier = modifier, modifier = modifier
) )
} }
} }
@ -65,7 +62,7 @@ class ChannelTypeProvider : PreviewParameterProvider<ChannelType> {
ChannelType.VoiceChannel, ChannelType.VoiceChannel,
ChannelType.SavedMessages, ChannelType.SavedMessages,
ChannelType.DirectMessage, ChannelType.DirectMessage,
ChannelType.Group, ChannelType.Group
) )
override val count: Int override val count: Int

View File

@ -28,7 +28,7 @@ fun ChannelSheetHeader(
channelName: String, channelName: String,
channelIcon: AutumnResource? = null, channelIcon: AutumnResource? = null,
channelType: ChannelType, channelType: ChannelType,
channelId: String, channelId: String
) { ) {
Row( Row(
modifier = Modifier.padding(vertical = 4.dp), modifier = Modifier.padding(vertical = 4.dp),
@ -65,7 +65,7 @@ fun ChannelSheetHeader(
text = channelName, text = channelName,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis
) )
} }
} }
@ -107,4 +107,4 @@ fun getIconBackColour(channelId: String): Color {
'Z' -> Color(0xFFFF8A80) 'Z' -> Color(0xFFFF8A80)
else -> Color(0xFFFFFFFF) else -> Color(0xFFFFFFFF)
} }
} }

View File

@ -47,11 +47,7 @@ fun replyContentText(message: Message): String {
} }
@Composable @Composable
fun ManageableReply( fun ManageableReply(reply: SendMessageReply, onToggleMention: () -> Unit, onRemove: () -> Unit) {
reply: SendMessageReply,
onToggleMention: () -> Unit,
onRemove: () -> Unit,
) {
val replyMessage = RevoltAPI.messageCache[reply.id] ?: return onRemove() val replyMessage = RevoltAPI.messageCache[reply.id] ?: return onRemove()
val replyAuthor = RevoltAPI.userCache[replyMessage.author] ?: return onRemove() val replyAuthor = RevoltAPI.userCache[replyMessage.author] ?: return onRemove()
@ -61,7 +57,7 @@ fun ManageableReply(
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
.horizontalScroll(rememberScrollState()) .horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp, vertical = 4.dp), .padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
@ -72,7 +68,7 @@ fun ManageableReply(
onRemove() onRemove()
} }
.padding(4.dp) .padding(4.dp)
.size(16.dp), .size(16.dp)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
@ -97,7 +93,7 @@ fun ManageableReply(
style = LocalTextStyle.current.copy( style = LocalTextStyle.current.copy(
brush = authorColour(message = replyMessage), brush = authorColour(message = replyMessage),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 12.sp, fontSize = 12.sp
) )
) )
@ -133,7 +129,7 @@ fun ManageableReply(
} else { } else {
MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f) MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)
}, },
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold
) )
} }
} }
@ -142,7 +138,7 @@ fun ManageableReply(
fun ReplyManager( fun ReplyManager(
replies: List<SendMessageReply>, replies: List<SendMessageReply>,
onToggleMention: (SendMessageReply) -> Unit, onToggleMention: (SendMessageReply) -> Unit,
onRemove: (SendMessageReply) -> Unit, onRemove: (SendMessageReply) -> Unit
) { ) {
Column { Column {
replies.forEach { reply -> replies.forEach { reply ->
@ -153,4 +149,4 @@ fun ReplyManager(
) )
} }
} }
} }

View File

@ -30,10 +30,7 @@ import chat.revolt.api.schemas.User
import chat.revolt.components.generic.UserAvatar import chat.revolt.components.generic.UserAvatar
@Composable @Composable
fun StackedUserAvatars( fun StackedUserAvatars(users: List<String>, amount: Int = 3) {
users: List<String>,
amount: Int = 3,
) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(16.dp + (8.dp * minOf(users.size, amount)), 16.dp) .size(16.dp + (8.dp * minOf(users.size, amount)), 16.dp)
@ -43,21 +40,19 @@ fun StackedUserAvatars(
UserAvatar( UserAvatar(
avatar = user?.avatar, avatar = user?.avatar,
userId = userId, userId = userId,
username = user?.let {User.resolveDefaultName(it)} ?: stringResource(id = R.string.unknown), username = user?.let { User.resolveDefaultName(it) } ?: stringResource(id = R.string.unknown),
size = 16.dp, size = 16.dp,
modifier = Modifier modifier = Modifier
.offset( .offset(
x = (index * 8).dp, x = (index * 8).dp
), )
) )
} }
} }
} }
@Composable @Composable
fun TypingIndicator( fun TypingIndicator(users: List<String>) {
users: List<String>,
) {
fun typingMessageResource(): Int { fun typingMessageResource(): Int {
return when (users.size) { return when (users.size) {
0 -> R.string.typing_blank 0 -> R.string.typing_blank
@ -84,11 +79,11 @@ fun TypingIndicator(
.clip( .clip(
RoundedCornerShape( RoundedCornerShape(
topStart = 16.dp, topStart = 16.dp,
topEnd = 16.dp, topEnd = 16.dp
) )
) )
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.background)
.padding(top = 4.dp, start = 16.dp, end = 16.dp), .padding(top = 4.dp, start = 16.dp, end = 16.dp)
) { ) {
StackedUserAvatars(users = users) StackedUserAvatars(users = users)
@ -103,8 +98,8 @@ fun TypingIndicator(
), ),
fontSize = 12.sp, fontSize = 12.sp,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis
) )
} }
} }
} }

View File

@ -84,13 +84,13 @@ fun RowScope.ChannelList(
currentChannel: String?, currentChannel: String?,
onChannelClick: (String) -> Unit, onChannelClick: (String) -> Unit,
onSpecialClick: (String) -> Unit, onSpecialClick: (String) -> Unit,
onServerSheetOpenFor: (String) -> Unit, onServerSheetOpenFor: (String) -> Unit
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val enableSmallBanner by remember { val enableSmallBanner by remember {
derivedStateOf { derivedStateOf {
lazyListState.firstVisibleItemScrollOffset > 40 || lazyListState.firstVisibleItemScrollOffset > 40 ||
lazyListState.firstVisibleItemIndex > 0 lazyListState.firstVisibleItemIndex > 0
} }
} }
@ -120,7 +120,7 @@ fun RowScope.ChannelList(
sheetState = channelContextSheetState, sheetState = channelContextSheetState,
onDismissRequest = { onDismissRequest = {
channelContextSheetShown = false channelContextSheetShown = false
}, }
) { ) {
ChannelContextSheet( ChannelContextSheet(
channelId = channelContextSheetTarget, channelId = channelContextSheetTarget,
@ -151,13 +151,13 @@ fun RowScope.ChannelList(
modifier = Modifier modifier = Modifier
.padding(start = 4.dp, top = 8.dp, bottom = 8.dp) .padding(start = 4.dp, top = 8.dp, bottom = 8.dp)
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.fillMaxWidth(), .fillMaxWidth()
) { ) {
LazyColumn( LazyColumn(
Modifier Modifier
.weight(1f) .weight(1f)
.fillMaxSize(), .fillMaxSize(),
state = lazyListState, state = lazyListState
) { ) {
if (serverId == "home") { if (serverId == "home") {
stickyHeader( stickyHeader(
@ -194,7 +194,7 @@ fun RowScope.ChannelList(
onClick = { onClick = {
onSpecialClick("home") onSpecialClick("home")
}, },
large = true, large = true
) )
} }
@ -212,7 +212,7 @@ fun RowScope.ChannelList(
onClick = { onClick = {
onChannelClick(notesChannelId ?: return@DrawerChannel) onChannelClick(notesChannelId ?: return@DrawerChannel)
}, },
large = true, large = true
) )
} }
@ -238,13 +238,19 @@ fun RowScope.ChannelList(
val channel = dmAbleChannels.getOrNull(it) ?: return@items val channel = dmAbleChannels.getOrNull(it) ?: return@items
val partner = val partner =
if (channel.channelType == ChannelType.DirectMessage) RevoltAPI.userCache[ChannelUtils.resolveDMPartner( if (channel.channelType == ChannelType.DirectMessage) {
channel RevoltAPI.userCache[
)] else null ChannelUtils.resolveDMPartner(
channel
)
]
} else {
null
}
DrawerChannel( DrawerChannel(
name = partner?.let { p -> User.resolveDefaultName(p) } ?: channel.name name = partner?.let { p -> User.resolveDefaultName(p) } ?: channel.name
?: stringResource(R.string.unknown), ?: stringResource(R.string.unknown),
channelType = channel.channelType ?: ChannelType.TextChannel, channelType = channel.channelType ?: ChannelType.TextChannel,
selected = currentDestination == "channel/{channelId}" && currentChannel == channel.id, selected = currentDestination == "channel/{channelId}" && currentChannel == channel.id,
hasUnread = channel.lastMessageID?.let { lastMessageID -> hasUnread = channel.lastMessageID?.let { lastMessageID ->
@ -313,7 +319,9 @@ fun RowScope.ChannelList(
Glide.with(this) Glide.with(this)
.load("$REVOLT_FILES/banners/${server.banner.id}") .load("$REVOLT_FILES/banners/${server.banner.id}")
.transition(DrawableTransitionOptions.withCrossFade()) .transition(
DrawableTransitionOptions.withCrossFade()
)
.into(this) .into(this)
} }
}, },
@ -348,21 +356,29 @@ fun RowScope.ChannelList(
Box( Box(
modifier = Modifier modifier = Modifier
.alpha(0.9f) .alpha(0.9f)
.height(BANNER_HEIGHT_COMPACT.dp + 8.dp) // due to padding in Text .height(
BANNER_HEIGHT_COMPACT.dp + 8.dp
) // due to padding in Text
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) .background(
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
)
) )
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
Spacer(Modifier.width(16.dp)) Spacer(Modifier.width(16.dp))
if (server?.flags has ServerFlags.Official) { if (server?.flags has ServerFlags.Official) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_revolt_decagram_24dp), painter = painterResource(
contentDescription = stringResource(R.string.server_flag_official), id = R.drawable.ic_revolt_decagram_24dp
),
contentDescription = stringResource(
R.string.server_flag_official
),
tint = if (server?.banner != null) { tint = if (server?.banner != null) {
bannerTextColour bannerTextColour
} else { } else {
@ -375,8 +391,12 @@ fun RowScope.ChannelList(
} }
if (server?.flags has ServerFlags.Verified) { if (server?.flags has ServerFlags.Verified) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_check_decagram_24dp), painter = painterResource(
contentDescription = stringResource(R.string.server_flag_verified), id = R.drawable.ic_check_decagram_24dp
),
contentDescription = stringResource(
R.string.server_flag_verified
),
tint = if (server?.banner != null) { tint = if (server?.banner != null) {
bannerTextColour bannerTextColour
} else { } else {
@ -389,8 +409,10 @@ fun RowScope.ChannelList(
} }
Text( Text(
text = (server?.name text = (
?: stringResource(R.string.unknown)), server?.name
?: stringResource(R.string.unknown)
),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = if (server?.banner != null) { color = if (server?.banner != null) {
bannerTextColour bannerTextColour
@ -438,7 +460,6 @@ fun RowScope.ChannelList(
} }
} }
} }
} }
if (categorisedChannels.isNullOrEmpty()) { if (categorisedChannels.isNullOrEmpty()) {
@ -458,7 +479,7 @@ fun RowScope.ChannelList(
Text( Text(
text = stringResource(R.string.no_channels_body), text = stringResource(R.string.no_channels_body),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center, textAlign = TextAlign.Center
) )
} }
} }
@ -480,9 +501,15 @@ fun RowScope.ChannelList(
val channel = item.channel val channel = item.channel
val partner = val partner =
if (channel.channelType == ChannelType.DirectMessage) RevoltAPI.userCache[ChannelUtils.resolveDMPartner( if (channel.channelType == ChannelType.DirectMessage) {
channel RevoltAPI.userCache[
)] else null ChannelUtils.resolveDMPartner(
channel
)
]
} else {
null
}
DrawerChannel( DrawerChannel(
name = partner?.let { p -> User.resolveDefaultName(p) } name = partner?.let { p -> User.resolveDefaultName(p) }
@ -498,7 +525,11 @@ fun RowScope.ChannelList(
} ?: false, } ?: false,
dmPartnerIcon = partner?.avatar ?: channel.icon, dmPartnerIcon = partner?.avatar ?: channel.icon,
dmPartnerId = partner?.id, dmPartnerId = partner?.id,
dmPartnerName = partner?.let { p -> User.resolveDefaultName(p) }, dmPartnerName = partner?.let { p ->
User.resolveDefaultName(
p
)
},
dmPartnerStatus = presenceFromStatus( dmPartnerStatus = presenceFromStatus(
status = partner?.status?.presence, status = partner?.status?.presence,
online = partner?.online ?: false online = partner?.online ?: false
@ -538,4 +569,4 @@ fun RowScope.ChannelList(
} }
} }
} }
} }

View File

@ -46,11 +46,14 @@ fun DrawerChannel(
dmPartnerName: String? = null, dmPartnerName: String? = null,
dmPartnerIcon: AutumnResource? = null, dmPartnerIcon: AutumnResource? = null,
dmPartnerId: String? = null, dmPartnerId: String? = null,
large: Boolean = false, large: Boolean = false
) { ) {
val backgroundColor = animateColorAsState( val backgroundColor = animateColorAsState(
if (selected) MaterialTheme.colorScheme.background if (selected) {
else Color.Transparent, MaterialTheme.colorScheme.background
} else {
Color.Transparent
},
animationSpec = spring(), animationSpec = spring(),
label = "Channel background colour" label = "Channel background colour"
) )
@ -102,12 +105,16 @@ fun DrawerChannel(
else -> ChannelIcon( else -> ChannelIcon(
channelType = channelType, channelType = channelType,
modifier = Modifier.then( modifier = Modifier.then(
if (large) Modifier.padding( if (large) {
end = 12.dp, Modifier.padding(
start = 4.dp, end = 12.dp,
top = 4.dp, start = 4.dp,
bottom = 4.dp top = 4.dp,
) else Modifier.padding(end = 8.dp) bottom = 4.dp
)
} else {
Modifier.padding(end = 8.dp)
}
) )
) )
} }
@ -136,4 +143,4 @@ fun DrawerChannel(
Spacer(modifier = Modifier.size(8.dp)) Spacer(modifier = Modifier.size(8.dp))
} }
} }
} }

View File

@ -41,7 +41,7 @@ fun DrawerServer(
) { ) {
if (iconId != null) { if (iconId != null) {
RemoteImage( RemoteImage(
url = "$REVOLT_FILES/icons/${iconId}/server.png?max_side=256", url = "$REVOLT_FILES/icons/$iconId/server.png?max_side=256",
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.size(48.dp) .size(48.dp)
@ -75,4 +75,4 @@ fun DrawerServer(
.background(LocalContentColor.current) .background(LocalContentColor.current)
) )
} }
} }

View File

@ -12,10 +12,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
fun DrawerServerlikeIcon( fun DrawerServerlikeIcon(onClick: () -> Unit, content: @Composable () -> Unit) {
onClick: () -> Unit,
content: @Composable () -> Unit
) {
IconButton( IconButton(
onClick = onClick, onClick = onClick,
modifier = Modifier modifier = Modifier
@ -26,4 +23,4 @@ fun DrawerServerlikeIcon(
) { ) {
content() content()
} }
} }

View File

@ -23,4 +23,4 @@ fun ServerDrawerSeparator() {
) )
) )
) )
} }

View File

@ -29,7 +29,7 @@ fun LinkOnHome(
icon: @Composable (Modifier) -> Unit, icon: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
description: @Composable () -> Unit = {}, description: @Composable () -> Unit = {},
onClick: () -> Unit, onClick: () -> Unit
) { ) {
Box( Box(
modifier = modifier modifier = modifier
@ -37,12 +37,12 @@ fun LinkOnHome(
.clickable { onClick() } .clickable { onClick() }
.padding(20.dp) .padding(20.dp)
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 5.dp), .padding(vertical = 5.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
) { ) {
icon(Modifier.padding(end = 14.dp)) icon(Modifier.padding(end = 14.dp))
@ -78,4 +78,4 @@ fun LinkOnHomePreview() {
icon = { mod -> Icon(Icons.Default.AccountBox, null, modifier = mod) }, icon = { mod -> Icon(Icons.Default.AccountBox, null, modifier = mod) },
onClick = { } onClick = { }
) )
} }

View File

@ -17,10 +17,7 @@ import chat.revolt.R
import chat.revolt.screens.about.Library import chat.revolt.screens.about.Library
@Composable @Composable
fun AttributionItem( fun AttributionItem(library: Library, onClick: () -> Unit) {
library: Library,
onClick: () -> Unit
) {
SelectionContainer { SelectionContainer {
Column( Column(
modifier = Modifier modifier = Modifier
@ -30,16 +27,15 @@ fun AttributionItem(
) { ) {
Text( Text(
text = library.name, text = library.name,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = stringResource(id = R.string.oss_attribution_tap_to_view_license), text = stringResource(id = R.string.oss_attribution_tap_to_view_license),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall
) )
} }
} }
} }

View File

@ -97,9 +97,11 @@ fun RawUserOverview(user: User, profile: Profile? = null) {
width = 4.dp, width = 4.dp,
brush = teamMemberFlair brush = teamMemberFlair
?: Brush.solidColor(Color.Transparent), ?: Brush.solidColor(Color.Transparent),
shape = MaterialTheme.shapes.large, shape = MaterialTheme.shapes.large
) )
} else Modifier } else {
Modifier
}
) )
) { ) {
profile?.background?.let { background -> profile?.background?.let { background ->
@ -138,7 +140,7 @@ fun RawUserOverview(user: User, profile: Profile? = null) {
userId = user.id ?: ULID.makeSpecial(0), userId = user.id ?: ULID.makeSpecial(0),
avatar = user.avatar, avatar = user.avatar,
size = 48.dp, size = 48.dp,
presence = presenceFromStatus(user.status?.presence), presence = presenceFromStatus(user.status?.presence)
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
@ -156,8 +158,8 @@ fun RawUserOverview(user: User, profile: Profile? = null) {
append("#${user.discriminator}") append("#${user.discriminator}")
pop() pop()
}.toAnnotatedString(), }.toAnnotatedString(),
color = if (profile?.background != null) Color.White else LocalContentColor.current, color = if (profile?.background != null) Color.White else LocalContentColor.current
) )
} }
} }
} }

View File

@ -26,10 +26,12 @@ fun ThemeChip(
.clickable(onClick = onClick) .clickable(onClick = onClick)
.then(modifier) .then(modifier)
.then( .then(
if (selected) if (selected) {
Modifier Modifier
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)) .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
else Modifier } else {
Modifier
}
) )
.padding(4.dp) .padding(4.dp)
.padding(8.dp) .padding(8.dp)
@ -58,4 +60,4 @@ fun SelectedThemeChipPreview() {
selected = true, selected = true,
onClick = {} onClick = {}
) )
} }

View File

@ -17,9 +17,7 @@ import androidx.compose.ui.unit.sp
import chat.revolt.R import chat.revolt.R
@Composable @Composable
fun DisconnectedScreen( fun DisconnectedScreen(onRetry: () -> Unit) {
onRetry: () -> Unit
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -35,7 +33,7 @@ fun DisconnectedScreen(
textAlign = TextAlign.Center textAlign = TextAlign.Center
), ),
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth()
) )
Text( Text(
@ -43,7 +41,7 @@ fun DisconnectedScreen(
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f), color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f),
style = MaterialTheme.typography.titleMedium.copy( style = MaterialTheme.typography.titleMedium.copy(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal
), ),
modifier = Modifier modifier = Modifier
.padding(vertical = 10.dp, horizontal = 20.dp) .padding(vertical = 10.dp, horizontal = 20.dp)
@ -60,4 +58,4 @@ fun DisconnectedScreen(
@Composable @Composable
fun DisconnectedScreenPreview() { fun DisconnectedScreenPreview() {
DisconnectedScreen(onRetry = {}) DisconnectedScreen(onRetry = {})
} }

View File

@ -9,7 +9,7 @@ import kotlinx.serialization.Serializable
data class Changelog( data class Changelog(
val summary: String, val summary: String,
val version: String, val version: String,
val date: String, val date: String
) )
@Serializable @Serializable
@ -26,20 +26,28 @@ class Changelogs(val context: Context, val kvStorage: KVStorage? = null) {
} }
fun getChangelog(version: String): String { fun getChangelog(version: String): String {
return context.assets.open("changelogs/${version}.md").use { return context.assets.open("changelogs/$version.md").use {
it.reader().readText() it.reader().readText()
} }
} }
suspend fun hasSeenLatest(): Boolean { suspend fun hasSeenLatest(): Boolean {
if (kvStorage == null) throw IllegalStateException("Not supported for non-KVStorage instances of Changelogs") if (kvStorage == null) {
throw IllegalStateException(
"Not supported for non-KVStorage instances of Changelogs"
)
}
return kvStorage.get("latestChangelogRead") == index.latest return kvStorage.get("latestChangelogRead") == index.latest
} }
suspend fun markAsSeen() { suspend fun markAsSeen() {
if (kvStorage == null) throw IllegalStateException("Not supported for non-KVStorage instances of Changelogs") if (kvStorage == null) {
throw IllegalStateException(
"Not supported for non-KVStorage instances of Changelogs"
)
}
kvStorage.set("latestChangelogRead", index.latest) kvStorage.set("latestChangelogRead", index.latest)
} }
} }

View File

@ -15,13 +15,13 @@ data class Emoji(
val alternates: List<List<Long>>, val alternates: List<List<Long>>,
val emoticons: List<String>, val emoticons: List<String>,
val shortcodes: List<String>, val shortcodes: List<String>,
val animated: Boolean, val animated: Boolean
) )
@Serializable @Serializable
data class EmojiGroup( data class EmojiGroup(
val group: String, val group: String,
val emoji: List<Emoji>, val emoji: List<Emoji>
) )
enum class FitzpatrickSkinTone(val modifierCodepoint: Int?) { enum class FitzpatrickSkinTone(val modifierCodepoint: Int?) {
@ -30,7 +30,7 @@ enum class FitzpatrickSkinTone(val modifierCodepoint: Int?) {
MediumLight(0x1F3FC), MediumLight(0x1F3FC),
Medium(0x1F3FD), Medium(0x1F3FD),
MediumDark(0x1F3FE), MediumDark(0x1F3FE),
Dark(0x1F3FF), Dark(0x1F3FF)
} }
enum class UnicodeEmojiSection(val googleName: String, val nameResource: Int) { enum class UnicodeEmojiSection(val googleName: String, val nameResource: Int) {
@ -42,7 +42,7 @@ enum class UnicodeEmojiSection(val googleName: String, val nameResource: Int) {
Activities("Activities and events", R.string.emoji_category_activities), Activities("Activities and events", R.string.emoji_category_activities),
Objects("Objects", R.string.emoji_category_objects), Objects("Objects", R.string.emoji_category_objects),
Symbols("Symbols", R.string.emoji_category_symbols), Symbols("Symbols", R.string.emoji_category_symbols),
Flags("Flags", R.string.emoji_category_flags), Flags("Flags", R.string.emoji_category_flags)
} }
sealed class Category { sealed class Category {
@ -55,7 +55,7 @@ sealed class EmojiPickerItem {
data class UnicodeEmoji( data class UnicodeEmoji(
val character: String, val character: String,
val hasSkinTones: Boolean, val hasSkinTones: Boolean,
val alternates: List<List<Long>>, val alternates: List<List<Long>>
) : EmojiPickerItem() ) : EmojiPickerItem()
data class ServerEmote(val emote: chat.revolt.api.schemas.Emoji) : EmojiPickerItem() data class ServerEmote(val emote: chat.revolt.api.schemas.Emoji) : EmojiPickerItem()
@ -106,17 +106,19 @@ class EmojiImpl {
val category = val category =
UnicodeEmojiSection.entries.find { it.googleName == group.group } ?: continue UnicodeEmojiSection.entries.find { it.googleName == group.group } ?: continue
list.add(EmojiPickerItem.Section(Category.UnicodeEmojiCategory(category))) list.add(EmojiPickerItem.Section(Category.UnicodeEmojiCategory(category)))
list.addAll(group.emoji.map { emoji -> list.addAll(
EmojiPickerItem.UnicodeEmoji( group.emoji.map { emoji ->
emoji.base.joinToString("") { String(Character.toChars(it.toInt())) }, EmojiPickerItem.UnicodeEmoji(
emoji.alternates.any { alternate -> emoji.base.joinToString("") { String(Character.toChars(it.toInt())) },
alternate.any { codepoint -> emoji.alternates.any { alternate ->
codepoint in 0x1F3FB..0x1F3FF alternate.any { codepoint ->
} codepoint in 0x1F3FB..0x1F3FF
}, }
emoji.alternates },
) emoji.alternates
}) )
}
)
} }
return list return list
@ -143,7 +145,9 @@ class EmojiImpl {
for (server in serversWithEmotes()) { for (server in serversWithEmotes()) {
val index = val index =
flatPickerList.indexOfFirst { it is EmojiPickerItem.Section && it.category is Category.ServerEmoteCategory && it.category.server == server } flatPickerList.indexOfFirst {
it is EmojiPickerItem.Section && it.category is Category.ServerEmoteCategory && it.category.server == server
}
val allEmotesInThatServer = val allEmotesInThatServer =
RevoltAPI.emojiCache.values.filter { it.parent?.id == server.id } RevoltAPI.emojiCache.values.filter { it.parent?.id == server.id }
val lastIndex = index + allEmotesInThatServer.size val lastIndex = index + allEmotesInThatServer.size
@ -152,12 +156,16 @@ class EmojiImpl {
} }
for (section in UnicodeEmojiSection.entries) { for (section in UnicodeEmojiSection.entries) {
val index = val index =
flatPickerList.indexOfFirst { it is EmojiPickerItem.Section && it.category is Category.UnicodeEmojiCategory && it.category.definition == section } flatPickerList.indexOfFirst {
it is EmojiPickerItem.Section && it.category is Category.UnicodeEmojiCategory && it.category.definition == section
}
val lastIndex = if (section == UnicodeEmojiSection.entries.last()) { val lastIndex = if (section == UnicodeEmojiSection.entries.last()) {
Int.MAX_VALUE Int.MAX_VALUE
} else { } else {
val nextSection = UnicodeEmojiSection.entries[section.ordinal + 1] val nextSection = UnicodeEmojiSection.entries[section.ordinal + 1]
flatPickerList.indexOfFirst { it is EmojiPickerItem.Section && it.category is Category.UnicodeEmojiCategory && it.category.definition == nextSection } - 1 flatPickerList.indexOfFirst {
it is EmojiPickerItem.Section && it.category is Category.UnicodeEmojiCategory && it.category.definition == nextSection
} - 1
} }
output[Category.UnicodeEmojiCategory(section)] = Pair(index, lastIndex) output[Category.UnicodeEmojiCategory(section)] = Pair(index, lastIndex)
} }
@ -220,17 +228,19 @@ class EmojiImpl {
val category = val category =
UnicodeEmojiSection.entries.find { it.googleName == group.group } ?: continue UnicodeEmojiSection.entries.find { it.googleName == group.group } ?: continue
list.add(EmojiPickerItem.Section(Category.UnicodeEmojiCategory(category))) list.add(EmojiPickerItem.Section(Category.UnicodeEmojiCategory(category)))
list.addAll(matchingEmoji.map { emoji -> list.addAll(
EmojiPickerItem.UnicodeEmoji( matchingEmoji.map { emoji ->
emoji.base.joinToString("") { String(Character.toChars(it.toInt())) }, EmojiPickerItem.UnicodeEmoji(
emoji.alternates.any { alternate -> emoji.base.joinToString("") { String(Character.toChars(it.toInt())) },
alternate.any { codepoint -> emoji.alternates.any { alternate ->
codepoint in 0x1F3FB..0x1F3FF alternate.any { codepoint ->
} codepoint in 0x1F3FB..0x1F3FF
}, }
emoji.alternates },
) emoji.alternates
}) )
}
)
} }
} }
@ -240,4 +250,4 @@ class EmojiImpl {
init { init {
metadata = initMetadata(RevoltApplication.instance.applicationContext) metadata = initMetadata(RevoltApplication.instance.applicationContext)
} }
} }

View File

@ -6,4 +6,4 @@ object Platform {
fun needsShowClipboardNotification(): Boolean { fun needsShowClipboardNotification(): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.S return Build.VERSION.SDK_INT < Build.VERSION_CODES.S
} }
} }

View File

@ -24,4 +24,4 @@ fun LifecycleEffect(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) ->
lifecycle.removeObserver(observer) lifecycle.removeObserver(observer)
} }
} }
} }

View File

@ -15,7 +15,7 @@ import com.discord.simpleast.core.node.Node
// https://github.com/discord/SimpleAST/blob/567b61c51056cbdec39e839100690c576c26a4c6/app/src/main/java/com/discord/simpleast/sample/spans/BlockBackgroundNode.kt // https://github.com/discord/SimpleAST/blob/567b61c51056cbdec39e839100690c576c26a4c6/app/src/main/java/com/discord/simpleast/sample/spans/BlockBackgroundNode.kt
// LICENSED UNDER THE APACHE LICENSE, VERSION 2.0 // LICENSED UNDER THE APACHE LICENSE, VERSION 2.0
// Adapted for Revolt. // Adapted for Revolt.
// //
// Changes: // Changes:
// - Fill and stroke colours are now parameters // - Fill and stroke colours are now parameters
// - Whether we are in a quote is no longer a boolean, but an integer with the quote depth // - Whether we are in a quote is no longer a boolean, but an integer with the quote depth
@ -28,7 +28,7 @@ class BlockBackgroundNode<R>(
private val quoteDepth: Int, private val quoteDepth: Int,
private val fillColor: Int = Color.DKGRAY, private val fillColor: Int = Color.DKGRAY,
private val strokeColor: Int = Color.BLACK, private val strokeColor: Int = Color.BLACK,
vararg children: Node<R>, vararg children: Node<R>
) : Node.Parent<R>(*children) { ) : Node.Parent<R>(*children) {
override fun render(builder: SpannableStringBuilder, renderContext: R) { override fun render(builder: SpannableStringBuilder, renderContext: R) {
@ -41,7 +41,8 @@ class BlockBackgroundNode<R>(
ensureEndsWithNewline(builder) ensureEndsWithNewline(builder)
val backgroundSpan = BlockBackgroundSpan( val backgroundSpan = BlockBackgroundSpan(
fillColor, strokeColor, fillColor,
strokeColor,
strokeWidth = 2, strokeWidth = 2,
strokeRadius = 15, strokeRadius = 15,
leftMargin = 40 * quoteDepth leftMargin = 40 * quoteDepth
@ -131,4 +132,4 @@ class BlockBackgroundSpan(
draw(canvas) draw(canvas)
} }
} }
} }

View File

@ -9,8 +9,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class EmoteSpan(drawable: Drawable) : class EmoteSpan(drawable: Drawable) :
ImageSpan(drawable, ALIGN_BOTTOM) { ImageSpan(drawable, ALIGN_BOTTOM)
}
class EmoteClickableSpan(private val emoteId: String) : LongClickableSpan() { class EmoteClickableSpan(private val emoteId: String) : LongClickableSpan() {
override fun onClick(widget: View) { override fun onClick(widget: View) {
@ -22,4 +21,4 @@ class EmoteClickableSpan(private val emoteId: String) : LongClickableSpan() {
override fun onLongClick(view: View?) { override fun onLongClick(view: View?) {
// no-op // no-op
} }
} }

View File

@ -20,9 +20,11 @@ class LinkSpan(private val url: String, private val drawBackground: Boolean = fa
// Intercept invite links // Intercept invite links
if (uri.host == Uri.parse(REVOLT_INVITES).host!! || if (uri.host == Uri.parse(REVOLT_INVITES).host!! ||
(uri.host?.endsWith(Uri.parse(REVOLT_APP).host!!) == true && uri.path?.startsWith( (
"/invite" uri.host?.endsWith(Uri.parse(REVOLT_APP).host!!) == true && uri.path?.startsWith(
) == true) "/invite"
) == true
)
) { ) {
val intent = Intent( val intent = Intent(
widget.context, widget.context,
@ -81,4 +83,4 @@ class LinkSpan(private val url: String, private val drawBackground: Boolean = fa
ds.bgColor = ds.linkColor and 0x33ffffff // 20% alpha ds.bgColor = ds.linkColor and 0x33ffffff // 20% alpha
} }
} }
} }

View File

@ -19,11 +19,7 @@ abstract class LongClickableSpan : ClickableSpan() {
class LongClickLinkMovementMethod : LinkMovementMethod() { class LongClickLinkMovementMethod : LinkMovementMethod() {
private var longClickHandler: Handler? = null private var longClickHandler: Handler? = null
private var isLongPressed = false private var isLongPressed = false
override fun onTouchEvent( override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
widget: TextView,
buffer: Spannable,
event: MotionEvent
): Boolean {
val action = event.action val action = event.action
if (action == MotionEvent.ACTION_CANCEL) { if (action == MotionEvent.ACTION_CANCEL) {
longClickHandler?.removeCallbacksAndMessages(null) longClickHandler?.removeCallbacksAndMessages(null)
@ -41,7 +37,8 @@ class LongClickLinkMovementMethod : LinkMovementMethod() {
val line = layout.getLineForVertical(y) val line = layout.getLineForVertical(y)
val off = layout.getOffsetForHorizontal(line, x.toFloat()) val off = layout.getOffsetForHorizontal(line, x.toFloat())
val link = buffer.getSpans( val link = buffer.getSpans(
off, off, off,
off,
LongClickableSpan::class.java LongClickableSpan::class.java
) )
if (link.isNotEmpty()) { if (link.isNotEmpty()) {
@ -82,4 +79,4 @@ class LongClickLinkMovementMethod : LinkMovementMethod() {
private var sInstance: LongClickLinkMovementMethod? = null private var sInstance: LongClickLinkMovementMethod? = null
} }
} }

View File

@ -17,5 +17,5 @@ data class MarkdownContext(
val channelMap: Map<String, String>, val channelMap: Map<String, String>,
val emojiMap: Map<String, Emoji>, val emojiMap: Map<String, Emoji>,
val serverId: String?, val serverId: String?,
val useLargeEmojis: Boolean, val useLargeEmojis: Boolean
) )

View File

@ -17,7 +17,7 @@ class UserMentionNode(private val userId: String) : Node<MarkdownContext>() {
override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) { override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) {
val content = renderContext.memberMap[userId]?.let { "@$it" } val content = renderContext.memberMap[userId]?.let { "@$it" }
?: renderContext.userMap[userId]?.let { "@${it.username}" } ?: renderContext.userMap[userId]?.let { "@${it.username}" }
?: "<@${userId}>" ?: "<@$userId>"
builder.append(content) builder.append(content)
builder.setSpan( builder.setSpan(
@ -58,7 +58,7 @@ class CustomEmoteNode(private val emoteId: String, private val context: Context)
Node<MarkdownContext>() { Node<MarkdownContext>() {
override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) { override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) {
val content = renderContext.emojiMap[emoteId]?.let { ":${it.name}:" } val content = renderContext.emojiMap[emoteId]?.let { ":${it.name}:" }
?: ":${emoteId}:" ?: ":$emoteId:"
val isGif = renderContext.emojiMap[emoteId]?.animated ?: false val isGif = renderContext.emojiMap[emoteId]?.animated ?: false
val emoteUrl = "$REVOLT_FILES/emojis/$emoteId/emote${if (isGif) ".gif" else ".png"}" val emoteUrl = "$REVOLT_FILES/emojis/$emoteId/emote${if (isGif) ".gif" else ".png"}"
@ -128,4 +128,4 @@ class LinkNode(val content: String, val url: String = content) : Node<MarkdownCo
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
) )
} }
} }

View File

@ -65,7 +65,8 @@ class TimestampRule<S>(private val context: Context) :
matcher.group(2) matcher.group(2)
), ),
listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance)) listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance))
), state ),
state
) )
} }
} }
@ -113,10 +114,7 @@ fun <RC, S> createInlineCodeRule(context: Context, backgroundColor: Int): Rule<R
) )
} }
fun <RC> createCodeRule( fun <RC> createCodeRule(context: Context, backgroundColor: Int): Rule<RC, Node<RC>, MarkdownState> {
context: Context,
backgroundColor: Int
): Rule<RC, Node<RC>, MarkdownState> {
val codeStyleProviders = CodeStyleProviders<RC>( val codeStyleProviders = CodeStyleProviders<RC>(
defaultStyleProvider = { listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance)) }, defaultStyleProvider = { listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance)) },
commentStyleProvider = { commentStyleProvider = {
@ -174,7 +172,7 @@ fun <RC> createCodeRule(
R.style.Code_TextAppearance_Params R.style.Code_TextAppearance_Params
) )
) )
}, }
) )
val languageMap = CodeRules.createCodeLanguageMap<RC, MarkdownState>(codeStyleProviders) val languageMap = CodeRules.createCodeLanguageMap<RC, MarkdownState>(codeStyleProviders)
@ -203,6 +201,6 @@ fun MarkdownParser.addRevoltRules(context: Context): MarkdownParser {
CustomEmoteRule(context), CustomEmoteRule(context),
TimestampRule(context), TimestampRule(context),
NamedLinkRule(), NamedLinkRule(),
LinkRule(), LinkRule()
) )
} }

Some files were not shown because too many files have changed in this diff Show More