feat: move voice chat to experiments and improve UI, schemas
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
5ec47975e3
commit
da62bba946
|
|
@ -30,8 +30,8 @@ val material3Version = "1.4.0-alpha15"
|
|||
val androidXTestVersion = "1.6.1"
|
||||
|
||||
object LivekitVersion {
|
||||
val core = "2.16.0"
|
||||
val componentsCompose = "1.3.1"
|
||||
val core = "2.20.3"
|
||||
val componentsCompose = "1.4.0"
|
||||
}
|
||||
|
||||
fun property(fileName: String, propertyName: String, fallbackEnv: String? = null): String? {
|
||||
|
|
@ -297,8 +297,9 @@ dependencies {
|
|||
implementation("dev.snipme:highlights:1.0.0")
|
||||
|
||||
// Livekit - Commented out for now to not inflate the app size.
|
||||
/*implementation("io.livekit:livekit-android:${LivekitVersion.core}")
|
||||
implementation("io.livekit:livekit-android-compose-components:${LivekitVersion.componentsCompose}")*/
|
||||
implementation("io.livekit:livekit-android:${LivekitVersion.core}")
|
||||
implementation("io.livekit:livekit-android-camerax:${LivekitVersion.core}")
|
||||
implementation("io.livekit:livekit-android-compose-components:${LivekitVersion.componentsCompose}")
|
||||
|
||||
// Firebase - Cloud Messaging
|
||||
implementation(platform("com.google.firebase:firebase-bom:33.15.0"))
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import chat.stoat.api.realtime.RealtimeSocket
|
|||
import chat.stoat.api.routes.user.fetchSelf
|
||||
import chat.stoat.api.schemas.AutumnResource
|
||||
import chat.stoat.api.schemas.ChannelType
|
||||
import chat.stoat.api.schemas.ChannelVoiceState
|
||||
import chat.stoat.api.schemas.Emoji
|
||||
import chat.stoat.api.schemas.Message
|
||||
import chat.stoat.api.schemas.Server
|
||||
|
|
@ -155,6 +156,7 @@ object StoatAPI {
|
|||
val channelCache = mutableStateMapOf<String, ChannelSchema>()
|
||||
val emojiCache = mutableStateMapOf<String, Emoji>()
|
||||
val messageCache = mutableStateMapOf<String, Message>()
|
||||
val voiceStateCache = mutableStateMapOf<String, ChannelVoiceState>()
|
||||
|
||||
val members = Members()
|
||||
|
||||
|
|
|
|||
|
|
@ -244,6 +244,10 @@ object RealtimeSocket {
|
|||
val emojiMap = readyFrame.emojis.associateBy { it.id!! }
|
||||
StoatAPI.emojiCache.putAll(emojiMap)
|
||||
|
||||
logcat { "Adding voice states to cache." }
|
||||
val voiceStateMap = readyFrame.voiceStates.associateBy { it.id }
|
||||
StoatAPI.voiceStateCache.putAll(voiceStateMap)
|
||||
|
||||
Log.d("RealtimeSocket", "Registering push notification channels.")
|
||||
channelRegistrator.register()
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
|||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
@Serializable
|
||||
data class MessagesInChannel(
|
||||
|
|
@ -75,6 +76,7 @@ data class Channel(
|
|||
@SerialName("default_permissions")
|
||||
val defaultPermissions: PermissionDescription? = null,
|
||||
val nsfw: Boolean? = null,
|
||||
val voice: JsonElement? = null,
|
||||
val type: String? = null // this is _only_ used for websocket events!
|
||||
) {
|
||||
fun mergeWithPartial(partial: Channel): Channel {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import kotlinx.serialization.Serializable
|
|||
@Serializable
|
||||
data class ChannelVoiceState(
|
||||
val id: String,
|
||||
val participants: List<UserVoiceState>,
|
||||
val node: String,
|
||||
val participants: List<UserVoiceState> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
@ -17,4 +16,5 @@ data class UserVoiceState(
|
|||
@SerialName("is_publishing") val isPublishing: Boolean,
|
||||
val screensharing: Boolean,
|
||||
val camera: Boolean,
|
||||
@SerialName(value = "joined_at") val joinedAt: String? = null,
|
||||
)
|
||||
|
|
@ -30,6 +30,7 @@ object Experiments {
|
|||
val usePolar = ExperimentInstance(false)
|
||||
val enableServerIdentityOptions = ExperimentInstance(false)
|
||||
val useFinalMarkdownRenderer = ExperimentInstance(false)
|
||||
val useVoiceChats2p0 = ExperimentInstance(false)
|
||||
|
||||
suspend fun hydrateWithKv() {
|
||||
val kvStorage = KVStorage(StoatApplication.instance)
|
||||
|
|
@ -52,6 +53,9 @@ object Experiments {
|
|||
useFinalMarkdownRenderer.setEnabled(
|
||||
kvStorage.getBoolean("exp/useFinalMarkdownRenderer") == true
|
||||
)
|
||||
useVoiceChats2p0.setEnabled(
|
||||
kvStorage.getBoolean("exp/useVoiceChats2p0") == true
|
||||
)
|
||||
|
||||
if (useFinalMarkdownRenderer.isEnabled && useKotlinBasedMarkdownRenderer.isEnabled) {
|
||||
// if jbm and fm are enabled, fm takes precedence. this should not be possible in practice
|
||||
|
|
|
|||
|
|
@ -43,19 +43,6 @@ sealed class MassMentionsVariates {
|
|||
object Disabled : MassMentionsVariates()
|
||||
}
|
||||
|
||||
@FeatureFlag("VoiceChannels2_0")
|
||||
sealed class VoiceChannels2_0Variates {
|
||||
@Treatment(
|
||||
"Enable the new voice channels 2.0 for all users"
|
||||
)
|
||||
object Enabled : VoiceChannels2_0Variates()
|
||||
|
||||
@Treatment(
|
||||
"Disable the new voice channels 2.0 for all users"
|
||||
)
|
||||
object Disabled : VoiceChannels2_0Variates()
|
||||
}
|
||||
|
||||
@FeatureFlag("FinalMarkdown")
|
||||
sealed class FinalMarkdownVariates {
|
||||
@Treatment(
|
||||
|
|
@ -105,16 +92,6 @@ object FeatureFlags {
|
|||
is MassMentionsVariates.Disabled -> false
|
||||
}
|
||||
|
||||
@FeatureFlag("VoiceChannels2_0")
|
||||
var voiceChannels2_0 by mutableStateOf<VoiceChannels2_0Variates>(
|
||||
VoiceChannels2_0Variates.Disabled
|
||||
)
|
||||
val voiceChannels2_0Granted: Boolean
|
||||
get() = when (voiceChannels2_0) {
|
||||
is VoiceChannels2_0Variates.Enabled -> true
|
||||
is VoiceChannels2_0Variates.Disabled -> false
|
||||
}
|
||||
|
||||
@FeatureFlag("FinalMarkdown")
|
||||
var finalMarkdown by mutableStateOf<FinalMarkdownVariates>(
|
||||
FinalMarkdownVariates.Disabled
|
||||
|
|
|
|||
|
|
@ -6,16 +6,21 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.stoat.R
|
||||
import chat.stoat.api.StoatAPI
|
||||
import chat.stoat.api.schemas.User
|
||||
import chat.stoat.callbacks.Action
|
||||
import chat.stoat.callbacks.ActionChannel
|
||||
import chat.stoat.composables.screens.chat.StackedUserAvatars
|
||||
|
|
@ -24,6 +29,7 @@ import kotlinx.coroutines.launch
|
|||
@Composable
|
||||
fun JoinVoiceChannelButton(channelId: String, modifier: Modifier = Modifier) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val voiceStatesForChannel = StoatAPI.voiceStateCache[channelId]
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
|
@ -36,26 +42,47 @@ fun JoinVoiceChannelButton(channelId: String, modifier: Modifier = Modifier) {
|
|||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
StackedUserAvatars(
|
||||
listOf(
|
||||
"01FHGJ3NPP7XANQQH8C2BE44ZY",
|
||||
"01F1WKM5TK2V6KCZWR6DGBJDTZ",
|
||||
"01EX2NCWQ0CHS3QJF0FEQS1GR4"
|
||||
),
|
||||
size = 24.dp,
|
||||
offset = 12.dp,
|
||||
amount = 3,
|
||||
serverId = null
|
||||
)
|
||||
if (voiceStatesForChannel?.participants?.isNotEmpty() == true) {
|
||||
StackedUserAvatars(
|
||||
users = voiceStatesForChannel.participants.map { it.id },
|
||||
size = 24.dp,
|
||||
offset = 12.dp,
|
||||
amount = voiceStatesForChannel.participants.size,
|
||||
serverId = null
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.icn_voice_chat_24dp),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.voice_join_offering),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.voice_join_offering_description_other, Integer.MAX_VALUE),
|
||||
when {
|
||||
voiceStatesForChannel == null || voiceStatesForChannel.participants.isEmpty() ->
|
||||
stringResource(R.string.voice_join_offering_description_zero)
|
||||
|
||||
voiceStatesForChannel.participants.size == 1 ->
|
||||
stringResource(
|
||||
R.string.voice_join_offering_description_one,
|
||||
voiceStatesForChannel.participants[0].id.let {
|
||||
StoatAPI.userCache[it]?.let { u -> User.resolveDefaultName(u) }
|
||||
?: R.string.unknown
|
||||
})
|
||||
|
||||
else ->
|
||||
stringResource(
|
||||
R.string.voice_join_offering_description_other,
|
||||
voiceStatesForChannel.participants.size
|
||||
)
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,31 @@
|
|||
package chat.stoat.composables.voice
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import chat.stoat.R
|
||||
import chat.stoat.api.routes.misc.getRootRoute
|
||||
import chat.stoat.api.routes.voice.joinCall
|
||||
import io.livekit.android.compose.local.RoomLocal
|
||||
import io.livekit.android.compose.local.RoomScope
|
||||
import io.livekit.android.compose.state.rememberTracks
|
||||
import io.livekit.android.compose.ui.VideoTrackView
|
||||
import logcat.LogPriority
|
||||
import logcat.asLog
|
||||
import logcat.logcat
|
||||
|
|
@ -78,9 +93,7 @@ fun VoiceSheet(
|
|||
viewModel.getVoiceToken()
|
||||
}
|
||||
|
||||
// TODO - Voice channels are not supported yet
|
||||
LaunchedEffect(Unit) { onDisconnect() }
|
||||
/*RoomScope(
|
||||
RoomScope(
|
||||
url = viewModel.voiceLkNode,
|
||||
token = viewModel.voiceToken,
|
||||
audio = true,
|
||||
|
|
@ -133,5 +146,5 @@ fun VoiceSheet(
|
|||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ import chat.stoat.api.routes.channel.react
|
|||
import chat.stoat.api.routes.microservices.autumn.FileArgs
|
||||
import chat.stoat.api.schemas.ChannelType
|
||||
import chat.stoat.api.schemas.Message
|
||||
import chat.stoat.api.settings.FeatureFlags
|
||||
import chat.stoat.api.settings.Experiments
|
||||
import chat.stoat.callbacks.Action
|
||||
import chat.stoat.callbacks.ActionChannel
|
||||
import chat.stoat.composables.chat.DateDivider
|
||||
|
|
@ -943,8 +943,9 @@ fun ChannelScreen(
|
|||
}
|
||||
}
|
||||
|
||||
if (viewModel.channel?.channelType == ChannelType.VoiceChannel
|
||||
&& FeatureFlags.voiceChannels2_0Granted
|
||||
if (viewModel.channel?.voice != null &&
|
||||
channelPermissions has PermissionBit.Connect &&
|
||||
Experiments.useVoiceChats2p0.isEnabled
|
||||
) {
|
||||
JoinVoiceChannelButton(channelId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ class ExperimentsSettingsScreenViewModel : ViewModel() {
|
|||
usePolarChecked.value = Experiments.usePolar.isEnabled
|
||||
enableServerIdentityOptionsChecked.value =
|
||||
Experiments.enableServerIdentityOptions.isEnabled
|
||||
useVoiceChats2p0.value = Experiments.useVoiceChats2p0.isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,6 +149,16 @@ class ExperimentsSettingsScreenViewModel : ViewModel() {
|
|||
enableServerIdentityOptionsChecked.value = value
|
||||
}
|
||||
}
|
||||
|
||||
val useVoiceChats2p0 = mutableStateOf(false)
|
||||
|
||||
fun setUseVoiceChats2p0(value: Boolean) {
|
||||
viewModelScope.launch {
|
||||
kv.set("exp/useVoiceChats2p0", value)
|
||||
Experiments.useVoiceChats2p0.setEnabled(value)
|
||||
useVoiceChats2p0.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
|
|
@ -290,6 +301,22 @@ fun ExperimentsSettingsScreen(
|
|||
modifier = Modifier.clickable { viewModel.setEnableServerIdentityOptionsChecked(!viewModel.enableServerIdentityOptionsChecked.value) }
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text("Voice Chats 2.0")
|
||||
},
|
||||
supportingContent = {
|
||||
Text("Enable voice chats support.")
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = viewModel.useVoiceChats2p0.value,
|
||||
onCheckedChange = null
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable { viewModel.setUseVoiceChats2p0(!viewModel.useVoiceChats2p0.value) }
|
||||
)
|
||||
|
||||
Subcategory(
|
||||
title = {
|
||||
Text("Disable experiments")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M240,440L300,440L300,360L240,360L240,440ZM340,520L400,520L400,280L340,280L340,520ZM450,600L510,600L510,200L450,200L450,600ZM560,520L620,520L620,280L560,280L560,520ZM660,440L720,440L720,360L660,360L660,440ZM80,880L80,160Q80,127 103.5,103.5Q127,80 160,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L240,720L80,880ZM206,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L160,160Q160,160 160,160Q160,160 160,160L160,685L206,640ZM160,640L160,640L160,160Q160,160 160,160Q160,160 160,160L160,160Q160,160 160,160Q160,160 160,160L160,640Q160,640 160,640Q160,640 160,640Z"/>
|
||||
</vector>
|
||||
Loading…
Reference in New Issue