feat: channel settings and attachment rendering
- also general design improvements
This commit is contained in:
parent
d93b9f1bcb
commit
4a5365c3c1
|
|
@ -61,6 +61,7 @@ dependencies {
|
||||||
|
|
||||||
// Jetpack Compose
|
// Jetpack Compose
|
||||||
implementation "androidx.compose.ui:ui:$compose_libraries_version"
|
implementation "androidx.compose.ui:ui:$compose_libraries_version"
|
||||||
|
implementation "androidx.compose.ui:ui-util:$compose_libraries_version"
|
||||||
implementation 'androidx.compose.material3:material3:1.0.1'
|
implementation 'androidx.compose.material3:material3:1.0.1'
|
||||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_libraries_version"
|
implementation "androidx.compose.ui:ui-tooling-preview:$compose_libraries_version"
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,16 @@ object RealtimeSocket {
|
||||||
RevoltAPI.userCache[userUpdateFrame.id] =
|
RevoltAPI.userCache[userUpdateFrame.id] =
|
||||||
existing.mergeWithPartial(userUpdateFrame.data)
|
existing.mergeWithPartial(userUpdateFrame.data)
|
||||||
}
|
}
|
||||||
|
"ChannelUpdate" -> {
|
||||||
|
val channelUpdateFrame =
|
||||||
|
RevoltJson.decodeFromString(ChannelUpdateFrame.serializer(), rawFrame)
|
||||||
|
|
||||||
|
val existing = RevoltAPI.channelCache[channelUpdateFrame.id]
|
||||||
|
?: return // if we don't have the channel no point in updating it
|
||||||
|
|
||||||
|
RevoltAPI.channelCache[channelUpdateFrame.id] =
|
||||||
|
existing.mergeWithPartial(channelUpdateFrame.data)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.i("RealtimeSocket", "Unknown frame: $rawFrame")
|
Log.i("RealtimeSocket", "Unknown frame: $rawFrame")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -168,12 +168,6 @@ data class ServerDeleteFrame(
|
||||||
val id: String
|
val id: String
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ServerUserChoice(
|
|
||||||
val server: String,
|
|
||||||
val user: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ServerMemberUpdateFrame(
|
data class ServerMemberUpdateFrame(
|
||||||
val type: String = "ServerMemberUpdate",
|
val type: String = "ServerMemberUpdate",
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,16 @@ data class MessagesInChannel(
|
||||||
val members: List<Member>? = null
|
val members: List<Member>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ServerUserChoice(
|
||||||
|
val server: String,
|
||||||
|
val user: String,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Member(
|
data class Member(
|
||||||
@SerialName("_id")
|
@SerialName("_id")
|
||||||
val id: String? = null,
|
val id: ServerUserChoice? = null,
|
||||||
|
|
||||||
@SerialName("joined_at")
|
@SerialName("joined_at")
|
||||||
val joinedAt: String? = null,
|
val joinedAt: String? = null,
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,44 @@
|
||||||
package chat.revolt.components.chat
|
package chat.revolt.components.chat
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.revolt.api.REVOLT_BASE
|
import chat.revolt.api.REVOLT_BASE
|
||||||
import chat.revolt.api.REVOLT_FILES
|
import chat.revolt.api.REVOLT_FILES
|
||||||
import chat.revolt.api.RevoltAPI
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.schemas.AutumnResource
|
||||||
import chat.revolt.components.generic.RemoteImage
|
import chat.revolt.components.generic.RemoteImage
|
||||||
import chat.revolt.api.schemas.Message as MessageSchema
|
import chat.revolt.api.schemas.Message as MessageSchema
|
||||||
|
|
||||||
|
fun viewAttachmentInBrowser(ctx: android.content.Context, attachment: AutumnResource) {
|
||||||
|
val customTab = CustomTabsIntent
|
||||||
|
.Builder()
|
||||||
|
.build()
|
||||||
|
customTab.launchUrl(
|
||||||
|
ctx,
|
||||||
|
Uri.parse("$REVOLT_FILES/attachments/${attachment.id}/${attachment.filename}")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Message(
|
fun Message(
|
||||||
message: MessageSchema
|
message: MessageSchema
|
||||||
) {
|
) {
|
||||||
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
|
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
Row(modifier = Modifier.padding(8.dp)) {
|
Row(modifier = Modifier.padding(8.dp)) {
|
||||||
if (author.avatar != null) {
|
if (author.avatar != null) {
|
||||||
|
|
@ -55,6 +71,39 @@ fun Message(
|
||||||
text = it
|
text = it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message.attachments?.let {
|
||||||
|
if (message.attachments.isNotEmpty()) {
|
||||||
|
message.attachments.forEach { attachment ->
|
||||||
|
if (attachment.metadata?.type == "Image") {
|
||||||
|
RemoteImage(
|
||||||
|
url = "$REVOLT_FILES/attachments/${attachment.id}/image.png",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 5.dp)
|
||||||
|
.clickable {
|
||||||
|
viewAttachmentInBrowser(context, attachment)
|
||||||
|
},
|
||||||
|
width = attachment.metadata.width?.toInt() ?: 0,
|
||||||
|
height = attachment.metadata.height?.toInt() ?: 0,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
description = "Attached image ${attachment.filename}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = attachment.filename ?: "Attachment",
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.clickable {
|
||||||
|
viewAttachmentInBrowser(context, attachment)
|
||||||
|
}
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -28,9 +28,9 @@ fun CollapsibleCard(
|
||||||
Column {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.clickable { expanded = !expanded }
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp)
|
.padding(16.dp),
|
||||||
.clickable { expanded = !expanded },
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package chat.revolt.components.generic
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PageHeader(
|
||||||
|
text: String,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.displaySmall.copy(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Left,
|
||||||
|
fontSize = 24.sp
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 15.dp, vertical = 15.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PageHeaderPreview() {
|
||||||
|
PageHeader(text = "Page Header")
|
||||||
|
}
|
||||||
|
|
@ -19,15 +19,26 @@ fun RemoteImage(
|
||||||
url: String,
|
url: String,
|
||||||
description: String,
|
description: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentScale: ContentScale = ContentScale.Crop
|
contentScale: ContentScale = ContentScale.Crop,
|
||||||
|
width: Int = 0,
|
||||||
|
height: Int = 0,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
AsyncImage(
|
fun imageRequest() = run {
|
||||||
model = ImageRequest.Builder(context)
|
val builder = ImageRequest.Builder(context)
|
||||||
.data(url)
|
|
||||||
.crossfade(true)
|
.crossfade(true)
|
||||||
.build(),
|
.data(url)
|
||||||
|
|
||||||
|
if (width != 0 && height != 0) {
|
||||||
|
builder.size(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = imageRequest(),
|
||||||
imageLoader = ImageLoader.Builder(context)
|
imageLoader = ImageLoader.Builder(context)
|
||||||
.components {
|
.components {
|
||||||
if (Build.VERSION.SDK_INT >= 28) {
|
if (Build.VERSION.SDK_INT >= 28) {
|
||||||
|
|
@ -45,7 +56,7 @@ fun RemoteImage(
|
||||||
.build(),
|
.build(),
|
||||||
contentDescription = description,
|
contentDescription = description,
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package chat.revolt.components.screens.chat
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.AccountBox
|
||||||
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import chat.revolt.api.schemas.ChannelType
|
||||||
|
import chat.revolt.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChannelIcon(
|
||||||
|
channelType: ChannelType,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when (channelType) {
|
||||||
|
ChannelType.TextChannel -> {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_pound_24dp),
|
||||||
|
contentDescription = stringResource(R.string.channel_text),
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ChannelType.VoiceChannel -> {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_volume_up_24dp),
|
||||||
|
contentDescription = stringResource(R.string.channel_voice),
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ChannelType.SavedMessages -> {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_note_24dp),
|
||||||
|
contentDescription = stringResource(R.string.channel_notes),
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ChannelType.DirectMessage -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.AccountCircle,
|
||||||
|
contentDescription = stringResource(R.string.channel_dm),
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ChannelType.Group -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.AccountBox,
|
||||||
|
contentDescription = stringResource(R.string.channel_group),
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,20 +5,14 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.revolt.api.schemas.ChannelType
|
import chat.revolt.api.schemas.ChannelType
|
||||||
import chat.revolt.R
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DrawerChannel(
|
fun DrawerChannel(
|
||||||
|
|
@ -36,36 +30,7 @@ fun DrawerChannel(
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
when (channelType) {
|
ChannelIcon(channelType = channelType, modifier = Modifier.padding(end = 8.dp))
|
||||||
ChannelType.TextChannel -> {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
|
||||||
painter = painterResource(R.drawable.ic_pound_24dp),
|
|
||||||
contentDescription = stringResource(R.string.channel_text)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ChannelType.VoiceChannel -> {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
|
||||||
painter = painterResource(R.drawable.ic_volume_up_24dp),
|
|
||||||
contentDescription = stringResource(R.string.channel_voice)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ChannelType.SavedMessages -> {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
|
||||||
painter = painterResource(R.drawable.ic_note_24dp),
|
|
||||||
contentDescription = stringResource(R.string.channel_notes)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
|
||||||
imageVector = Icons.Default.List,
|
|
||||||
contentDescription = "Channel"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
text = name,
|
text = name,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ fun DisconnectedScreen(
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 20.dp, vertical = 10.dp)
|
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -46,7 +45,7 @@ fun DisconnectedScreen(
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 20.dp, vertical = 10.dp)
|
.padding(vertical = 10.dp, horizontal = 20.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,14 +55,14 @@ class SplashScreenViewModel @Inject constructor(
|
||||||
val connectivityManager =
|
val connectivityManager =
|
||||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
val networkCapabilities = connectivityManager.activeNetwork ?: return false
|
val network = connectivityManager.activeNetwork ?: return false
|
||||||
val actNw =
|
val capabilities =
|
||||||
connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
|
connectivityManager.getNetworkCapabilities(network) ?: return false
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||||
actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||||
actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,24 @@ class ChatRouterViewModel : ViewModel() {
|
||||||
_currentServer.value = serverId
|
_currentServer.value = serverId
|
||||||
}
|
}
|
||||||
|
|
||||||
fun goToHome() {
|
fun navigateToServer(serverId: String, navController: NavController) {
|
||||||
_currentServer.value = "home"
|
setCurrentServer(serverId)
|
||||||
|
|
||||||
|
if (serverId == "home") {
|
||||||
|
navController.navigate("home") {
|
||||||
|
popUpTo("home") {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val channelId = RevoltAPI.serverCache[serverId]?.channels?.firstOrNull()
|
||||||
|
navController.navigate("channel/$channelId") {
|
||||||
|
popUpTo("home") {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +101,9 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { viewModel.goToHome() },
|
onClick = {
|
||||||
|
viewModel.navigateToServer("home", navController)
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
|
|
@ -107,7 +125,12 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.clickable { viewModel.setCurrentServer(server.id!!) },
|
.clickable {
|
||||||
|
viewModel.navigateToServer(
|
||||||
|
server.id!!,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
},
|
||||||
description = "${server.name}"
|
description = "${server.name}"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -119,7 +142,12 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
.clickable { viewModel.setCurrentServer(server.id!!) }
|
.clickable {
|
||||||
|
viewModel.navigateToServer(
|
||||||
|
server.id!!,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = server.name.first().toString(),
|
text = server.name.first().toString(),
|
||||||
|
|
@ -143,11 +171,11 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
RevoltAPI.channelCache.values.filter { it.channelType == ChannelType.DirectMessage }
|
RevoltAPI.channelCache.values.filter { it.channelType == ChannelType.Group }
|
||||||
.forEach { channel ->
|
.forEach { channel ->
|
||||||
DrawerChannel(
|
DrawerChannel(
|
||||||
name = "DM #${channel.id}", // TODO get user or group name
|
name = channel.name ?: "GDM #${channel.id}",
|
||||||
channelType = ChannelType.DirectMessage,
|
channelType = ChannelType.Group,
|
||||||
selected = channel.id == (navBackStackEntry?.arguments?.getString(
|
selected = channel.id == (navBackStackEntry?.arguments?.getString(
|
||||||
"channelId"
|
"channelId"
|
||||||
) ?: false),
|
) ?: false),
|
||||||
|
|
@ -185,7 +213,11 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = vie
|
||||||
) == ch.id,
|
) == ch.id,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch { channelDrawerState.close() }
|
scope.launch { channelDrawerState.close() }
|
||||||
navController.navigate("channel/${ch.id}")
|
navController.navigate("channel/${ch.id}") {
|
||||||
|
popUpTo("home") {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,29 @@
|
||||||
package chat.revolt.screens.chat.views
|
package chat.revolt.screens.chat.views
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
|
@ -33,6 +44,9 @@ import chat.revolt.RevoltTweenFloat
|
||||||
import chat.revolt.RevoltTweenInt
|
import chat.revolt.RevoltTweenInt
|
||||||
import chat.revolt.api.routes.channel.fetchMessagesFromChannel
|
import chat.revolt.api.routes.channel.fetchMessagesFromChannel
|
||||||
import chat.revolt.components.chat.MessageField
|
import chat.revolt.components.chat.MessageField
|
||||||
|
import chat.revolt.components.generic.CollapsibleCard
|
||||||
|
import chat.revolt.components.generic.PageHeader
|
||||||
|
import chat.revolt.components.screens.chat.ChannelIcon
|
||||||
|
|
||||||
class ChannelScreenViewModel : ViewModel() {
|
class ChannelScreenViewModel : ViewModel() {
|
||||||
private var _channel by mutableStateOf<Channel?>(null)
|
private var _channel by mutableStateOf<Channel?>(null)
|
||||||
|
|
@ -109,6 +123,8 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_renderableMessages.clear()
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
fetchMessagesFromChannel(channel!!.id!!, limit = 50, false).let {
|
fetchMessagesFromChannel(channel!!.id!!, limit = 50, false).let {
|
||||||
it.messages!!.reversed().forEach { message ->
|
it.messages!!.reversed().forEach { message ->
|
||||||
|
|
@ -122,6 +138,29 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun fetchOlderMessages() {
|
||||||
|
if (channel == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
fetchMessagesFromChannel(
|
||||||
|
channel!!.id!!,
|
||||||
|
limit = 20,
|
||||||
|
true,
|
||||||
|
before = renderableMessages.first().id
|
||||||
|
).let {
|
||||||
|
it.messages!!.forEach { message ->
|
||||||
|
addUserIfUnknown(message.author!!)
|
||||||
|
if (!RevoltAPI.messageCache.containsKey(message.id)) {
|
||||||
|
RevoltAPI.messageCache[message.id!!] = message
|
||||||
|
}
|
||||||
|
_renderableMessages.add(0, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun fetchChannel(id: String) {
|
fun fetchChannel(id: String) {
|
||||||
if (id in RevoltAPI.channelCache) {
|
if (id in RevoltAPI.channelCache) {
|
||||||
_channel = RevoltAPI.channelCache[id]
|
_channel = RevoltAPI.channelCache[id]
|
||||||
|
|
@ -158,6 +197,79 @@ class ChannelScreenViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChannelInfoScreen(
|
||||||
|
channel: Channel,
|
||||||
|
viewModel: ChannelScreenViewModel,
|
||||||
|
onClosed: () -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val clipboardManager: ClipboardManager =
|
||||||
|
LocalClipboardManager.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
ChannelIcon(
|
||||||
|
channelType = channel.channelType!!,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
PageHeader(text = channel.name ?: channel.id!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
CollapsibleCard(title = "Advanced") {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text("Channel ID: ${channel.id}")
|
||||||
|
|
||||||
|
Button(onClick = {
|
||||||
|
clipboardManager.setText(AnnotatedString(channel.id!!))
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Copied",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}) {
|
||||||
|
Text("Copy ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
viewModel.fetchMessages()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Refetch messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onClosed,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ChannelScreen(
|
fun ChannelScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
|
|
@ -166,6 +278,8 @@ fun ChannelScreen(
|
||||||
) {
|
) {
|
||||||
val channel = viewModel.channel
|
val channel = viewModel.channel
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
val channelInfoOpen = remember { mutableStateOf(false) }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(channelId) {
|
LaunchedEffect(channelId) {
|
||||||
viewModel.fetchChannel(channelId)
|
viewModel.fetchChannel(channelId)
|
||||||
|
|
@ -184,20 +298,61 @@ fun ChannelScreen(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (channelInfoOpen.value) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
channelInfoOpen.value = false
|
||||||
|
},
|
||||||
|
properties = DialogProperties(
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ChannelInfoScreen(channel, viewModel) {
|
||||||
|
channelInfoOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
coroutineScope.launch {
|
||||||
|
channelInfoOpen.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
ChannelIcon(
|
||||||
|
channelType = channel.channelType!!,
|
||||||
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
text = channel.name ?: channel.id!!,
|
text = channel.name ?: channel.id!!,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
modifier = Modifier.padding(16.dp)
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyColumn(Modifier.weight(1f)) {
|
LazyColumn(Modifier.weight(1f)) {
|
||||||
|
item {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
viewModel.fetchOlderMessages()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp, horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
Text("Load older")
|
||||||
|
}
|
||||||
|
}
|
||||||
items(viewModel.renderableMessages) { message ->
|
items(viewModel.renderableMessages) { message ->
|
||||||
Message(message)
|
Message(message)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,8 @@ package chat.revolt.screens.chat.views
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
|
@ -19,6 +13,7 @@ import chat.revolt.components.screens.home.LinkOnHome
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import chat.revolt.R
|
import chat.revolt.R
|
||||||
|
import chat.revolt.components.generic.PageHeader
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -36,19 +31,8 @@ class HomeScreenViewModel @Inject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hiltViewModel()) {
|
fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hiltViewModel()) {
|
||||||
Column() {
|
Column {
|
||||||
Text(
|
PageHeader(text = stringResource(id = R.string.home))
|
||||||
text = stringResource(id = R.string.home),
|
|
||||||
style = MaterialTheme.typography.displaySmall.copy(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
textAlign = TextAlign.Left,
|
|
||||||
fontSize = 24.sp
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 15.dp, vertical = 15.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
|
|
||||||
LinkOnHome(
|
LinkOnHome(
|
||||||
heading = stringResource(id = R.string.logout),
|
heading = stringResource(id = R.string.logout),
|
||||||
icon = Icons.Default.Close,
|
icon = Icons.Default.Close,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
|
|
||||||
|
const val FORCE_ANDROID_DEFAULTS = false
|
||||||
|
|
||||||
const val FOREGROUND = 0xffffffff
|
const val FOREGROUND = 0xffffffff
|
||||||
|
|
||||||
val DarkColorScheme = darkColorScheme(
|
val DarkColorScheme = darkColorScheme(
|
||||||
|
|
@ -60,9 +62,15 @@ fun RevoltTheme(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(
|
if (FORCE_ANDROID_DEFAULTS) {
|
||||||
colorScheme = colorScheme,
|
MaterialTheme(
|
||||||
typography = RevoltTypography,
|
content = content
|
||||||
content = content
|
)
|
||||||
)
|
} else {
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = RevoltTypography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue