Merge branch 'revoltchat:dev' into feature-add-server-context-menu-long-press
This commit is contained in:
commit
579d3c7306
|
|
@ -27,10 +27,14 @@ val hiltVersion = "2.52"
|
|||
val glideVersion = "4.16.0"
|
||||
val ktorVersion = "3.0.0-beta-2"
|
||||
val media3Version = "1.7.1"
|
||||
val livekitVersion = "2.2.0"
|
||||
val material3Version = "1.4.0-alpha15"
|
||||
val androidXTestVersion = "1.6.1"
|
||||
|
||||
object LivekitVersion {
|
||||
val core = "2.16.0"
|
||||
val componentsCompose = "1.3.1"
|
||||
}
|
||||
|
||||
fun property(fileName: String, propertyName: String, fallbackEnv: String? = null): String? {
|
||||
val propsFile = rootProject.file(fileName)
|
||||
if (propsFile.exists()) {
|
||||
|
|
@ -294,9 +298,9 @@ dependencies {
|
|||
implementation("org.jetbrains:markdown:0.7.3")
|
||||
implementation("dev.snipme:highlights:1.0.0")
|
||||
|
||||
// Livekit
|
||||
// FIXME temporarily not included, re-add when realtime media is to be implemented
|
||||
// implementation "io.livekit:livekit-android:$livekit_version"
|
||||
// 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}")*/
|
||||
|
||||
// Firebase - Cloud Messaging
|
||||
implementation(platform("com.google.firebase:firebase-bom:33.15.0"))
|
||||
|
|
|
|||
|
|
@ -13,18 +13,32 @@ import android.view.Menu
|
|||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.EaseInOutExpo
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -33,14 +47,25 @@ import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSiz
|
|||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
|
@ -61,6 +86,8 @@ import chat.revolt.api.settings.Experiments
|
|||
import chat.revolt.api.settings.LoadedSettings
|
||||
import chat.revolt.api.settings.SyncedSettings
|
||||
import chat.revolt.composables.generic.HealthAlert
|
||||
import chat.revolt.composables.voice.VoicePermissionSwitch
|
||||
import chat.revolt.composables.voice.VoiceSheet
|
||||
import chat.revolt.material.EasingTokens
|
||||
import chat.revolt.ndk.NativeLibraries
|
||||
import chat.revolt.persistence.KVStorage
|
||||
|
|
@ -394,6 +421,8 @@ val RevoltTweenColour: FiniteAnimationSpec<Color> = tween(400, easing = EaseInOu
|
|||
val NavTweenInt: FiniteAnimationSpec<IntOffset> = tween(350, easing = EaseInOutExpo)
|
||||
val NavTweenFloat: FiniteAnimationSpec<Float> = tween(350, easing = EaseInOutExpo)
|
||||
|
||||
// This composable handles the main compose entrypoint of the app, provides the main navigation
|
||||
// graph, and handles the animation and layout for the voice chat UI.
|
||||
@Composable
|
||||
fun AppEntrypoint(
|
||||
windowSizeClass: WindowSizeClass,
|
||||
|
|
@ -408,251 +437,350 @@ fun AppEntrypoint(
|
|||
onRetryConnection: () -> Unit,
|
||||
onUpdateNextDestination: (String) -> Unit = {}
|
||||
) {
|
||||
var showVoiceUI by rememberSaveable { mutableStateOf(false) }
|
||||
var voiceChannelId by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
|
||||
val chatUIScale by animateFloatAsState(
|
||||
if (showVoiceUI) 0.8f else 1.0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
)
|
||||
)
|
||||
val chatUIOpacity by animateFloatAsState(
|
||||
if (showVoiceUI) 0.8f else 1.0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
)
|
||||
)
|
||||
|
||||
BackHandler(showVoiceUI) {
|
||||
showVoiceUI = false
|
||||
}
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(showVoiceUI) {
|
||||
if (showVoiceUI) keyboardController?.hide()
|
||||
}
|
||||
|
||||
val navController = rememberNavController()
|
||||
|
||||
RevoltTheme(
|
||||
requestedTheme = LoadedSettings.theme,
|
||||
colourOverrides = SyncedSettings.android.colourOverrides
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLowest)
|
||||
) {
|
||||
if (isHealthAlertActive) {
|
||||
healthNotice?.let {
|
||||
HealthAlert(notice = healthNotice, onDismiss = onDismissHealthAlert)
|
||||
}
|
||||
}
|
||||
|
||||
if (couldNotLogIn) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
// no-op
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.could_not_log_in_heading))
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.could_not_log_in_body))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismissLoginError()
|
||||
onRetryConnection()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.could_not_log_in_cta_try_again))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismissLoginError()
|
||||
onLogout()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.could_not_log_in_cta_logout))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "default",
|
||||
enterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||
animationSpec = NavTweenInt,
|
||||
initialOffset = { it / 3 }
|
||||
) + fadeIn(animationSpec = NavTweenFloat)
|
||||
},
|
||||
exitTransition = {
|
||||
slideOutOfContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||
animationSpec = NavTweenInt,
|
||||
targetOffset = { it / 3 }
|
||||
) + fadeOut(animationSpec = NavTweenFloat)
|
||||
},
|
||||
popEnterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Right,
|
||||
animationSpec = NavTweenInt,
|
||||
initialOffset = { it / 3 }
|
||||
) + fadeIn(animationSpec = NavTweenFloat)
|
||||
},
|
||||
popExitTransition = {
|
||||
slideOutOfContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Right,
|
||||
animationSpec = NavTweenInt,
|
||||
targetOffset = { it / 2 }
|
||||
) + fadeOut(animationSpec = NavTweenFloat)
|
||||
}
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.scale(chatUIScale)
|
||||
.alpha(chatUIOpacity),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
composable("default") {
|
||||
DefaultDestinationScreen(
|
||||
navController,
|
||||
nextDestination,
|
||||
isConnected,
|
||||
onRetryConnection
|
||||
)
|
||||
if (isHealthAlertActive) {
|
||||
healthNotice?.let {
|
||||
HealthAlert(notice = healthNotice, onDismiss = onDismissHealthAlert)
|
||||
}
|
||||
}
|
||||
|
||||
composable("login/greeting") { LoginGreetingScreen(navController) }
|
||||
composable("login/login") { LoginScreen(navController) }
|
||||
composable("login/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry ->
|
||||
val mfaTicket = backStackEntry.arguments?.getString("mfaTicket") ?: ""
|
||||
val allowedAuthTypes =
|
||||
backStackEntry.arguments?.getString("allowedAuthTypes") ?: ""
|
||||
|
||||
MfaScreen(navController, allowedAuthTypes, mfaTicket)
|
||||
}
|
||||
|
||||
composable("register/greeting") { RegisterGreetingScreen(navController) }
|
||||
composable("register/details") { RegisterDetailsScreen(navController) }
|
||||
composable("register/verify/{email}") { backStackEntry ->
|
||||
val email = backStackEntry.arguments?.getString("email") ?: ""
|
||||
|
||||
RegisterVerifyScreen(navController, email)
|
||||
}
|
||||
composable("register/onboarding") {
|
||||
OnboardingScreen(
|
||||
navController,
|
||||
onOnboardingComplete = {
|
||||
onUpdateNextDestination("chat")
|
||||
navController.popBackStack(
|
||||
navController.graph.startDestinationRoute!!,
|
||||
inclusive = true
|
||||
)
|
||||
navController.navigate("default")
|
||||
if (couldNotLogIn) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
// no-op
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.could_not_log_in_heading))
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.could_not_log_in_body))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismissLoginError()
|
||||
onRetryConnection()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.could_not_log_in_cta_try_again))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDismissLoginError()
|
||||
onLogout()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.could_not_log_in_cta_logout))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable("login2/init") { InitScreen(navController, windowSizeClass) }
|
||||
|
||||
// This is only used outside of Polar mode
|
||||
// Otherwise you may be looking for "main" right below
|
||||
composable(
|
||||
"chat",
|
||||
enterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Up,
|
||||
animationSpec = tween(
|
||||
400,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
),
|
||||
initialOffset = { it / 3 }
|
||||
) + fadeIn(animationSpec = RevoltTweenFloat)
|
||||
}
|
||||
) {
|
||||
ChatRouterScreen(
|
||||
navController,
|
||||
windowSizeClass,
|
||||
onNullifiedUser = {
|
||||
onRetryConnection()
|
||||
navController.popBackStack(
|
||||
navController.graph.startDestinationRoute!!,
|
||||
inclusive = true
|
||||
)
|
||||
navController.navigate("default")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// This is only the main screen in Polar mode
|
||||
// Otherwise you may be looking for "chat" right above
|
||||
composable(
|
||||
"main",
|
||||
enterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Up,
|
||||
animationSpec = tween(
|
||||
400,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
),
|
||||
initialOffset = { it / 3 }
|
||||
) + fadeIn(animationSpec = RevoltTweenFloat) + scaleIn(
|
||||
animationSpec = tween(
|
||||
400,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
),
|
||||
initialScale = 0.8f,
|
||||
transformOrigin = TransformOrigin.Center
|
||||
)
|
||||
}
|
||||
) {
|
||||
MainScreen(navController)
|
||||
}
|
||||
composable(
|
||||
"main/conversation/{channelId}",
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = "default",
|
||||
enterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||
animationSpec = tween(
|
||||
600,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
),
|
||||
initialOffset = { it }
|
||||
) + fadeIn(animationSpec = RevoltTweenFloat)
|
||||
animationSpec = NavTweenInt,
|
||||
initialOffset = { it / 3 }
|
||||
) + fadeIn(animationSpec = NavTweenFloat)
|
||||
},
|
||||
exitTransition = {
|
||||
slideOutOfContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||
animationSpec = NavTweenInt,
|
||||
targetOffset = { it / 3 }
|
||||
) + fadeOut(animationSpec = NavTweenFloat)
|
||||
},
|
||||
popEnterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Right,
|
||||
animationSpec = tween(
|
||||
600,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
),
|
||||
targetOffset = { it }
|
||||
) + fadeOut(animationSpec = RevoltTweenFloat)
|
||||
animationSpec = NavTweenInt,
|
||||
initialOffset = { it / 3 }
|
||||
) + fadeIn(animationSpec = NavTweenFloat)
|
||||
},
|
||||
popExitTransition = {
|
||||
slideOutOfContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Right,
|
||||
animationSpec = NavTweenInt,
|
||||
targetOffset = { it / 2 }
|
||||
) + fadeOut(animationSpec = NavTweenFloat)
|
||||
}
|
||||
) { backStackEntry ->
|
||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||
ChannelScreen(
|
||||
channelId = channelId,
|
||||
onToggleDrawer = {},
|
||||
useDrawer = false,
|
||||
useBackButton = true,
|
||||
backButtonAction = {
|
||||
navController.popBackStack()
|
||||
) {
|
||||
composable("default") {
|
||||
DefaultDestinationScreen(
|
||||
navController,
|
||||
nextDestination,
|
||||
isConnected,
|
||||
onRetryConnection
|
||||
)
|
||||
}
|
||||
|
||||
composable("login/greeting") { LoginGreetingScreen(navController) }
|
||||
composable("login/login") { LoginScreen(navController) }
|
||||
composable("login/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry ->
|
||||
val mfaTicket = backStackEntry.arguments?.getString("mfaTicket") ?: ""
|
||||
val allowedAuthTypes =
|
||||
backStackEntry.arguments?.getString("allowedAuthTypes") ?: ""
|
||||
|
||||
MfaScreen(navController, allowedAuthTypes, mfaTicket)
|
||||
}
|
||||
|
||||
composable("register/greeting") { RegisterGreetingScreen(navController) }
|
||||
composable("register/details") { RegisterDetailsScreen(navController) }
|
||||
composable("register/verify/{email}") { backStackEntry ->
|
||||
val email = backStackEntry.arguments?.getString("email") ?: ""
|
||||
|
||||
RegisterVerifyScreen(navController, email)
|
||||
}
|
||||
composable("register/onboarding") {
|
||||
OnboardingScreen(
|
||||
navController,
|
||||
onOnboardingComplete = {
|
||||
onUpdateNextDestination("chat")
|
||||
navController.popBackStack(
|
||||
navController.graph.startDestinationRoute!!,
|
||||
inclusive = true
|
||||
)
|
||||
navController.navigate("default")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable("login2/init") { InitScreen(navController, windowSizeClass) }
|
||||
|
||||
// This is only used outside of Polar mode
|
||||
// Otherwise you may be looking for "main" right below
|
||||
composable(
|
||||
"chat",
|
||||
enterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Up,
|
||||
animationSpec = tween(
|
||||
400,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
),
|
||||
initialOffset = { it / 3 }
|
||||
) + fadeIn(animationSpec = RevoltTweenFloat)
|
||||
}
|
||||
) {
|
||||
ChatRouterScreen(
|
||||
navController,
|
||||
windowSizeClass,
|
||||
disableBackHandler = showVoiceUI,
|
||||
onNullifiedUser = {
|
||||
onRetryConnection()
|
||||
navController.popBackStack(
|
||||
navController.graph.startDestinationRoute!!,
|
||||
inclusive = true
|
||||
)
|
||||
navController.navigate("default")
|
||||
},
|
||||
onEnterVoiceUI = { channelId ->
|
||||
showVoiceUI = true
|
||||
voiceChannelId = channelId
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// This is only the main screen in Polar mode
|
||||
// Otherwise you may be looking for "chat" right above
|
||||
composable(
|
||||
"main",
|
||||
enterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Up,
|
||||
animationSpec = tween(
|
||||
400,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
),
|
||||
initialOffset = { it / 3 }
|
||||
) + fadeIn(animationSpec = RevoltTweenFloat) + scaleIn(
|
||||
animationSpec = tween(
|
||||
400,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
),
|
||||
initialScale = 0.8f,
|
||||
transformOrigin = TransformOrigin.Center
|
||||
)
|
||||
}
|
||||
) {
|
||||
MainScreen(navController)
|
||||
}
|
||||
composable(
|
||||
"main/conversation/{channelId}",
|
||||
enterTransition = {
|
||||
slideIntoContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Left,
|
||||
animationSpec = tween(
|
||||
600,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
),
|
||||
initialOffset = { it }
|
||||
) + fadeIn(animationSpec = RevoltTweenFloat)
|
||||
},
|
||||
useChatUI = true
|
||||
exitTransition = {
|
||||
slideOutOfContainer(
|
||||
AnimatedContentTransitionScope.SlideDirection.Right,
|
||||
animationSpec = tween(
|
||||
600,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
),
|
||||
targetOffset = { it }
|
||||
) + fadeOut(animationSpec = RevoltTweenFloat)
|
||||
}
|
||||
) { backStackEntry ->
|
||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||
ChannelScreen(
|
||||
channelId = channelId,
|
||||
onToggleDrawer = {},
|
||||
useDrawer = false,
|
||||
useBackButton = true,
|
||||
backButtonAction = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
useChatUI = true
|
||||
)
|
||||
}
|
||||
|
||||
composable("create/group") { CreateGroupScreen(navController) }
|
||||
|
||||
composable("discover") { DiscoverScreen(navController) }
|
||||
|
||||
composable("settings") { SettingsScreen(navController) }
|
||||
composable("settings/profile") { ProfileSettingsScreen(navController) }
|
||||
composable("settings/sessions") { SessionSettingsScreen(navController) }
|
||||
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
|
||||
composable("settings/chat") { ChatSettingsScreen(navController) }
|
||||
composable("settings/debug") { DebugSettingsScreen(navController) }
|
||||
composable("settings/experiments") { ExperimentsSettingsScreen(navController) }
|
||||
composable("settings/changelogs") { ChangelogsSettingsScreen(navController) }
|
||||
composable("settings/language") { LanguagePickerSettingsScreen(navController) }
|
||||
|
||||
composable("settings/channel/{channelId}") { backStackEntry ->
|
||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||
ChannelSettingsHome(navController, channelId)
|
||||
}
|
||||
composable("settings/channel/{channelId}/overview") { backStackEntry ->
|
||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||
ChannelSettingsOverview(navController, channelId)
|
||||
}
|
||||
composable("settings/channel/{channelId}/permissions") { backStackEntry ->
|
||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||
ChannelSettingsPermissions(navController, channelId)
|
||||
}
|
||||
|
||||
composable("about") { AboutScreen(navController) }
|
||||
composable("about/oss") { AttributionScreen(navController) }
|
||||
|
||||
composable("labs") { LabsRootScreen(navController) }
|
||||
}
|
||||
}
|
||||
|
||||
if (showVoiceUI) { // if tapped outside the voice UI, close it
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
showVoiceUI = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = showVoiceUI,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { it -> it },
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
)
|
||||
),
|
||||
exit = slideOutVertically(
|
||||
targetOffsetY = { it -> it },
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = EasingTokens.EmphasizedDecelerate
|
||||
)
|
||||
)
|
||||
) {
|
||||
// We need a box as applying the padding elsewhere leads to either
|
||||
// janky animation or layout
|
||||
Box(Modifier.safeDrawingPadding()) {
|
||||
Card(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.widthIn(max = 600.dp)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
VoicePermissionSwitch(
|
||||
onCancel = {
|
||||
showVoiceUI = false
|
||||
}
|
||||
) {
|
||||
voiceChannelId?.let {
|
||||
VoiceSheet(
|
||||
it,
|
||||
onDisconnect = {
|
||||
showVoiceUI = false
|
||||
voiceChannelId = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
composable("create/group") { CreateGroupScreen(navController) }
|
||||
|
||||
composable("discover") { DiscoverScreen(navController) }
|
||||
|
||||
composable("settings") { SettingsScreen(navController) }
|
||||
composable("settings/profile") { ProfileSettingsScreen(navController) }
|
||||
composable("settings/sessions") { SessionSettingsScreen(navController) }
|
||||
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
|
||||
composable("settings/chat") { ChatSettingsScreen(navController) }
|
||||
composable("settings/debug") { DebugSettingsScreen(navController) }
|
||||
composable("settings/experiments") { ExperimentsSettingsScreen(navController) }
|
||||
composable("settings/changelogs") { ChangelogsSettingsScreen(navController) }
|
||||
composable("settings/language") { LanguagePickerSettingsScreen(navController) }
|
||||
|
||||
composable("settings/channel/{channelId}") { backStackEntry ->
|
||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||
ChannelSettingsHome(navController, channelId)
|
||||
}
|
||||
composable("settings/channel/{channelId}/overview") { backStackEntry ->
|
||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||
ChannelSettingsOverview(navController, channelId)
|
||||
}
|
||||
composable("settings/channel/{channelId}/permissions") { backStackEntry ->
|
||||
val channelId = backStackEntry.arguments?.getString("channelId") ?: ""
|
||||
ChannelSettingsPermissions(navController, channelId)
|
||||
}
|
||||
|
||||
composable("about") { AboutScreen(navController) }
|
||||
composable("about/oss") { AttributionScreen(navController) }
|
||||
|
||||
composable("labs") { LabsRootScreen(navController) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,14 +53,20 @@ import kotlinx.serialization.json.Json
|
|||
import java.net.SocketException
|
||||
import chat.revolt.api.schemas.Channel as ChannelSchema
|
||||
|
||||
const val REVOLT_BASE = "https://api.revolt.chat/0.8"
|
||||
private const val USE_ALPHA_API = false
|
||||
|
||||
val REVOLT_BASE =
|
||||
if (USE_ALPHA_API) "https://alpha.revolt.chat/api" else "https://api.revolt.chat/0.8"
|
||||
const val REVOLT_SUPPORT = "https://support.revolt.chat"
|
||||
const val REVOLT_MARKETING = "https://revolt.chat"
|
||||
const val REVOLT_FILES = "https://cdn.revoltusercontent.com"
|
||||
const val REVOLT_JANUARY = "https://jan.revolt.chat"
|
||||
val REVOLT_FILES =
|
||||
if (USE_ALPHA_API) "https://alpha.revolt.chat/autumn" else "https://cdn.revoltusercontent.com"
|
||||
val REVOLT_JANUARY =
|
||||
if (USE_ALPHA_API) "https://alpha.revolt.chat/january" else "https://jan.revolt.chat"
|
||||
const val REVOLT_APP = "https://app.revolt.chat"
|
||||
const val REVOLT_INVITES = "https://rvlt.gg"
|
||||
const val REVOLT_WEBSOCKET = "wss://ws.revolt.chat"
|
||||
val REVOLT_WEBSOCKET =
|
||||
if (USE_ALPHA_API) "wss://alpha.revolt.chat/ws" else "wss://ws.revolt.chat"
|
||||
const val REVOLT_KJBOOK = "https://revoltchat.github.io/android"
|
||||
|
||||
fun String.api(): String {
|
||||
|
|
@ -68,7 +74,9 @@ fun String.api(): String {
|
|||
}
|
||||
|
||||
fun buildUserAgent(accessMethod: String = "Ktor"): String {
|
||||
return "$accessMethod RevoltAndroid/${BuildConfig.VERSION_NAME} ${BuildConfig.APPLICATION_ID} (Android ${android.os.Build.VERSION.SDK_INT}; ${android.os.Build.MANUFACTURER} ${android.os.Build.DEVICE}; (Kotlin ${KotlinVersion.CURRENT})"
|
||||
return "$accessMethod RevoltAndroid/${BuildConfig.VERSION_NAME} " +
|
||||
"${BuildConfig.APPLICATION_ID} Android/${android.os.Build.VERSION.SDK_INT} " +
|
||||
"(${android.os.Build.MANUFACTURER} ${android.os.Build.DEVICE}) Kotlin/${KotlinVersion.CURRENT}"
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ enum class PermissionBit(val value: Long) {
|
|||
MuteMembers(1L shl 33),
|
||||
DeafenMembers(1L shl 34),
|
||||
MoveMembers(1L shl 35),
|
||||
Listen(1L shl 36),
|
||||
|
||||
// % 1 bit reserved
|
||||
|
||||
|
|
@ -91,7 +92,8 @@ object BitDefaults {
|
|||
PermissionBit.SendEmbeds +
|
||||
PermissionBit.UploadFiles +
|
||||
PermissionBit.Connect +
|
||||
PermissionBit.Speak
|
||||
PermissionBit.Speak +
|
||||
PermissionBit.Listen
|
||||
|
||||
val SavedMessages =
|
||||
PermissionBit.GrantAllSafe.value
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import io.ktor.websocket.readText
|
|||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.serialization.SerializationException
|
||||
import logcat.logcat
|
||||
|
||||
enum class DisconnectionState {
|
||||
Disconnected,
|
||||
|
|
@ -149,10 +150,14 @@ object RealtimeSocket {
|
|||
|
||||
"Ready" -> {
|
||||
val readyFrame = RevoltJson.decodeFromString(ReadyFrame.serializer(), rawFrame)
|
||||
Log.d(
|
||||
"RealtimeSocket",
|
||||
"Received ready frame with ${readyFrame.users.size} users, ${readyFrame.servers.size} servers, ${readyFrame.channels.size} channels, and ${readyFrame.emojis.size} emojis."
|
||||
)
|
||||
|
||||
logcat {
|
||||
"Received ready frame with ${readyFrame.users.size} users, " +
|
||||
"${readyFrame.servers.size} servers, " +
|
||||
"${readyFrame.channels.size} channels, " +
|
||||
"${readyFrame.emojis.size} emojis, " +
|
||||
"and ${readyFrame.voiceStates.size} voice states."
|
||||
}
|
||||
|
||||
Log.d("RealtimeSocket", "Adding users to cache.")
|
||||
val userMap = readyFrame.users.associateBy { it.id!! }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package chat.revolt.api.realtime.frames.receivable
|
||||
|
||||
import chat.revolt.api.schemas.Channel
|
||||
import chat.revolt.api.schemas.ChannelVoiceState
|
||||
import chat.revolt.api.schemas.Embed
|
||||
import chat.revolt.api.schemas.Emoji
|
||||
import chat.revolt.api.schemas.Member
|
||||
|
|
@ -42,7 +43,8 @@ data class ReadyFrame(
|
|||
val users: List<User>,
|
||||
val servers: List<Server>,
|
||||
val channels: List<Channel>,
|
||||
val emojis: List<Emoji>
|
||||
val emojis: List<Emoji>,
|
||||
@SerialName("voice_states") val voiceStates: List<ChannelVoiceState> = listOf(),
|
||||
)
|
||||
|
||||
typealias MessageFrame = Message
|
||||
|
|
|
|||
|
|
@ -18,36 +18,48 @@ data class Root(
|
|||
|
||||
@Serializable
|
||||
data class Features(
|
||||
val captcha: CAPTCHA,
|
||||
val captcha: CAPTCHAFeature,
|
||||
val email: Boolean,
|
||||
|
||||
@SerialName("invite_only")
|
||||
val inviteOnly: Boolean,
|
||||
|
||||
val autumn: Autumn,
|
||||
val january: Autumn,
|
||||
val voso: Voso
|
||||
@SerialName("invite_only") val inviteOnly: Boolean,
|
||||
val autumn: AutumnJanuaryFeature,
|
||||
val january: AutumnJanuaryFeature,
|
||||
val voso: LegacyVoiceFeature? = null,
|
||||
val livekit: LiveKitFeature? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Autumn(
|
||||
data class AutumnJanuaryFeature(
|
||||
val enabled: Boolean,
|
||||
val url: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CAPTCHA(
|
||||
data class CAPTCHAFeature(
|
||||
val enabled: Boolean,
|
||||
val key: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Voso(
|
||||
data class LegacyVoiceFeature(
|
||||
val enabled: Boolean,
|
||||
val url: String,
|
||||
val ws: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LiveKitFeature(
|
||||
val enabled: Boolean,
|
||||
val nodes: List<LiveKitNode>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LiveKitNode(
|
||||
val name: String,
|
||||
val lat: Double,
|
||||
val lon: Double,
|
||||
@SerialName("public_url") val publicUrl: String,
|
||||
)
|
||||
|
||||
suspend fun getRootRoute(): Root {
|
||||
return RevoltHttp.get("/".api()).body()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
package chat.revolt.api.routes.voice
|
||||
|
||||
import chat.revolt.api.RevoltError
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import chat.revolt.api.RevoltJson
|
||||
import chat.revolt.api.api
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
|
||||
@Serializable
|
||||
data class JoinCallResponse(
|
||||
val token: String,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
suspend fun joinCall(channelId: String, nodeName: String): JoinCallResponse {
|
||||
val response = RevoltHttp.post("/channels/$channelId/join_call".api()) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(mapOf("node" to nodeName))
|
||||
}.bodyAsText()
|
||||
|
||||
try {
|
||||
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response)
|
||||
throw Exception(error.type)
|
||||
} catch (e: SerializationException) {
|
||||
// Not an error
|
||||
}
|
||||
|
||||
return RevoltJson.decodeFromString(JoinCallResponse.serializer(), response)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package chat.revolt.api.schemas
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ChannelVoiceState(
|
||||
val id: String,
|
||||
val participants: List<UserVoiceState>,
|
||||
val node: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserVoiceState(
|
||||
val id: String,
|
||||
@SerialName("is_receiving") val isReceiving: Boolean,
|
||||
@SerialName("is_publishing") val isPublishing: Boolean,
|
||||
val screensharing: Boolean,
|
||||
val camera: Boolean,
|
||||
)
|
||||
|
|
@ -17,19 +17,6 @@ sealed class LabsAccessControlVariates {
|
|||
data class Restricted(val predicate: () -> Boolean) : LabsAccessControlVariates()
|
||||
}
|
||||
|
||||
@FeatureFlag("MediaConversations")
|
||||
sealed class MediaConversationsVariates {
|
||||
@Treatment(
|
||||
"Enable voice, video and screen conversations for all users"
|
||||
)
|
||||
object Enabled : MediaConversationsVariates()
|
||||
|
||||
@Treatment(
|
||||
"Enable voice, video and screen conversations for users that meet certain or all criteria (implementation-specific)"
|
||||
)
|
||||
data class Restricted(val predicate: () -> Boolean) : MediaConversationsVariates()
|
||||
}
|
||||
|
||||
@FeatureFlag("UserCards")
|
||||
sealed class UserCardsVariates {
|
||||
@Treatment(
|
||||
|
|
@ -56,6 +43,19 @@ 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()
|
||||
}
|
||||
|
||||
object FeatureFlags {
|
||||
@FeatureFlag("LabsAccessControl")
|
||||
var labsAccessControl by mutableStateOf<LabsAccessControlVariates>(
|
||||
|
|
@ -69,19 +69,6 @@ object FeatureFlags {
|
|||
is LabsAccessControlVariates.Restricted -> (labsAccessControl as LabsAccessControlVariates.Restricted).predicate()
|
||||
}
|
||||
|
||||
@FeatureFlag("MediaConversations")
|
||||
var mediaConversations by mutableStateOf<MediaConversationsVariates>(
|
||||
MediaConversationsVariates.Restricted {
|
||||
RevoltAPI.selfId == SpecialUsers.JENNIFER
|
||||
}
|
||||
)
|
||||
|
||||
val mediaConversationsGranted: Boolean
|
||||
get() = when (mediaConversations) {
|
||||
is MediaConversationsVariates.Enabled -> true
|
||||
is MediaConversationsVariates.Restricted -> (mediaConversations as MediaConversationsVariates.Restricted).predicate()
|
||||
}
|
||||
|
||||
@FeatureFlag("UserCards")
|
||||
var userCards by mutableStateOf<UserCardsVariates>(
|
||||
UserCardsVariates.Restricted {
|
||||
|
|
@ -97,11 +84,21 @@ object FeatureFlags {
|
|||
|
||||
@FeatureFlag("MassMentions")
|
||||
var massMentions by mutableStateOf<MassMentionsVariates>(
|
||||
MassMentionsVariates.Enabled
|
||||
MassMentionsVariates.Disabled
|
||||
)
|
||||
val massMentionsGranted: Boolean
|
||||
get() = when (massMentions) {
|
||||
is MassMentionsVariates.Enabled -> true
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,19 +8,21 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialShapes
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.toShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -184,7 +186,7 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
|||
when (type) {
|
||||
SystemMessageType.CHANNEL_OWNERSHIP_CHANGED -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_key_arrow_right_24dp),
|
||||
painter = painterResource(R.drawable.icn_key_24dp),
|
||||
contentDescription = stringResource(R.string.system_message_ownership_changed_alt),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = modifier.size(size)
|
||||
|
|
@ -193,7 +195,7 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
|||
|
||||
SystemMessageType.CHANNEL_ICON_CHANGED -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_image_multiple_24dp),
|
||||
painter = painterResource(R.drawable.icn_landscape_2_edit_24dp),
|
||||
contentDescription = stringResource(
|
||||
R.string.system_message_channel_icon_changed_alt
|
||||
),
|
||||
|
|
@ -204,7 +206,7 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
|||
|
||||
SystemMessageType.CHANNEL_DESCRIPTION_CHANGED -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_text_box_multiple_24dp),
|
||||
painter = painterResource(R.drawable.icn_contract_edit_24dp),
|
||||
contentDescription = stringResource(
|
||||
R.string.system_message_channel_description_changed_alt
|
||||
),
|
||||
|
|
@ -215,7 +217,7 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
|||
|
||||
SystemMessageType.CHANNEL_RENAMED -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_cursor_text_24dp),
|
||||
painter = painterResource(R.drawable.icn_ink_highlighter_move_24dp),
|
||||
contentDescription = stringResource(R.string.system_message_channel_renamed_alt),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = modifier.size(size)
|
||||
|
|
@ -224,7 +226,7 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
|||
|
||||
SystemMessageType.USER_REMOVE -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_account_cancel_24dp),
|
||||
painter = painterResource(R.drawable.icn_group_remove_24dp),
|
||||
contentDescription = stringResource(R.string.system_message_user_removed_alt),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = modifier.size(size)
|
||||
|
|
@ -233,7 +235,7 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
|||
|
||||
SystemMessageType.USER_ADDED -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_account_plus_24dp),
|
||||
painter = painterResource(R.drawable.icn_group_add_24dp),
|
||||
contentDescription = stringResource(R.string.system_message_user_added_alt),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = modifier.size(size)
|
||||
|
|
@ -242,7 +244,7 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
|||
|
||||
SystemMessageType.USER_BANNED -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_gavel_24dp),
|
||||
painter = painterResource(R.drawable.icn_gavel_24dp),
|
||||
contentDescription = stringResource(R.string.system_message_user_banned_alt),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = modifier.size(size)
|
||||
|
|
@ -251,7 +253,7 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
|||
|
||||
SystemMessageType.USER_KICKED -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_shield_24dp),
|
||||
painter = painterResource(R.drawable.icn_sports_and_outdoors_24dp),
|
||||
contentDescription = stringResource(R.string.system_message_user_kicked_alt),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = modifier.size(size)
|
||||
|
|
@ -260,7 +262,7 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
|||
|
||||
SystemMessageType.USER_LEFT -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_account_arrow_left_24dp),
|
||||
painter = painterResource(R.drawable.icn_door_open_24dp),
|
||||
contentDescription = stringResource(R.string.system_message_user_left_alt),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = modifier.size(size)
|
||||
|
|
@ -269,16 +271,16 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
|||
|
||||
SystemMessageType.USER_JOINED -> {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_account_arrow_right_24dp),
|
||||
painter = painterResource(R.drawable.icn_waving_hand_24dp),
|
||||
contentDescription = stringResource(R.string.system_message_user_joined_alt),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = modifier.size(size)
|
||||
modifier = modifier.size(size),
|
||||
)
|
||||
}
|
||||
|
||||
SystemMessageType.TEXT -> {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
painter = painterResource(R.drawable.icn_info_24dp),
|
||||
contentDescription = stringResource(R.string.system_message_text_alt),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = modifier.size(size)
|
||||
|
|
@ -287,6 +289,36 @@ fun SystemMessageIcon(type: SystemMessageType, modifier: Modifier = Modifier, si
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
private fun shapeForType(type: SystemMessageType): Shape {
|
||||
return when (type) {
|
||||
SystemMessageType.CHANNEL_OWNERSHIP_CHANGED -> MaterialShapes.Slanted
|
||||
SystemMessageType.CHANNEL_ICON_CHANGED -> MaterialShapes.Pentagon
|
||||
SystemMessageType.CHANNEL_DESCRIPTION_CHANGED -> MaterialShapes.Gem
|
||||
SystemMessageType.CHANNEL_RENAMED -> MaterialShapes.Pill
|
||||
SystemMessageType.USER_REMOVE -> MaterialShapes.Flower
|
||||
SystemMessageType.USER_ADDED -> MaterialShapes.Sunny
|
||||
SystemMessageType.USER_BANNED -> MaterialShapes.Burst
|
||||
SystemMessageType.USER_KICKED -> MaterialShapes.SoftBurst
|
||||
SystemMessageType.USER_LEFT -> MaterialShapes.Cookie4Sided
|
||||
SystemMessageType.USER_JOINED -> MaterialShapes.Cookie9Sided
|
||||
SystemMessageType.TEXT -> MaterialShapes.Square
|
||||
}.toShape()
|
||||
}
|
||||
|
||||
// TODO - find the best colours for each type
|
||||
@Composable
|
||||
private fun backgroundColourForType(type: SystemMessageType): Color {
|
||||
return MaterialTheme.colorScheme.primaryContainer
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun contentColourForType(type: SystemMessageType): Color {
|
||||
return MaterialTheme.colorScheme.onPrimaryContainer
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SystemMessageIconWithBackground(
|
||||
type: SystemMessageType,
|
||||
|
|
@ -296,10 +328,16 @@ fun SystemMessageIconWithBackground(
|
|||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.3f))
|
||||
.clip(shapeForType(type))
|
||||
.background(
|
||||
color = backgroundColourForType(type),
|
||||
)
|
||||
.size(size)
|
||||
) {
|
||||
SystemMessageIcon(type = type)
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides contentColourForType(type),
|
||||
) {
|
||||
SystemMessageIcon(type = type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.revolt.R
|
||||
|
|
@ -29,10 +30,16 @@ import chat.revolt.api.schemas.User
|
|||
import chat.revolt.composables.generic.UserAvatar
|
||||
|
||||
@Composable
|
||||
fun StackedUserAvatars(users: List<String>, amount: Int = 3, serverId: String?) {
|
||||
fun StackedUserAvatars(
|
||||
users: List<String>,
|
||||
amount: Int = 3,
|
||||
size: Dp = 16.dp,
|
||||
offset: Dp = 8.dp,
|
||||
serverId: String?
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(16.dp + (8.dp * minOf(users.size, amount)), 16.dp)
|
||||
.size(size + (offset * minOf(users.size, amount)), size)
|
||||
) {
|
||||
users.take(amount).forEachIndexed { index, userId ->
|
||||
val user = RevoltAPI.userCache[userId]
|
||||
|
|
@ -44,10 +51,10 @@ fun StackedUserAvatars(users: List<String>, amount: Int = 3, serverId: String?)
|
|||
username = user?.let { User.resolveDefaultName(it) }
|
||||
?: stringResource(id = R.string.unknown),
|
||||
rawUrl = maybeMember?.avatar?.let { "$REVOLT_FILES/avatars/${it.id}" },
|
||||
size = 16.dp,
|
||||
size = size,
|
||||
modifier = Modifier
|
||||
.offset(
|
||||
x = (index * 8).dp
|
||||
x = (index * offset.value).dp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -91,7 +98,7 @@ fun TypingIndicator(users: List<String>, serverId: String?) {
|
|||
RevoltAPI.userCache[userId]?.let { u ->
|
||||
val maybeMember =
|
||||
serverId?.let { RevoltAPI.members.getMember(serverId, userId) }
|
||||
|
||||
|
||||
maybeMember?.nickname ?: User.resolveDefaultName(u)
|
||||
} ?: userId
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package chat.revolt.composables.screens.chat.drawer
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.VisibilityThreshold
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
|
|
@ -28,6 +30,7 @@ import androidx.compose.foundation.layout.offset
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
|
|
@ -71,6 +74,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
|
|
@ -96,6 +100,7 @@ import chat.revolt.composables.generic.UserAvatar
|
|||
import chat.revolt.composables.generic.presenceFromStatus
|
||||
import chat.revolt.composables.screens.chat.ChannelIcon
|
||||
import chat.revolt.screens.chat.ChatRouterDestination
|
||||
import chat.revolt.screens.chat.LocalIsConnected
|
||||
import chat.revolt.sheets.ChannelContextSheet
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -154,6 +159,17 @@ fun ChannelSideDrawer(
|
|||
), label = "Server banner height"
|
||||
)
|
||||
|
||||
val serverInfoOffset by animateDpAsState(
|
||||
if (LocalIsConnected.current)
|
||||
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
else
|
||||
0.dp,
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessMediumLow,
|
||||
visibilityThreshold = Dp.VisibilityThreshold
|
||||
)
|
||||
)
|
||||
|
||||
// - Take the list of servers and filter them by the ones that are in the ordering.
|
||||
// - Sort the servers that are in the ordering using the ordering.
|
||||
// - Add the servers that aren't in the ordering to the end of the list.
|
||||
|
|
@ -202,30 +218,40 @@ fun ChannelSideDrawer(
|
|||
)
|
||||
) {
|
||||
stickyHeader(key = "self") {
|
||||
UserAvatar(
|
||||
username = RevoltAPI.userCache[RevoltAPI.selfId]?.let {
|
||||
User.resolveDefaultName(
|
||||
it
|
||||
Column(Modifier.background(MaterialTheme.colorScheme.background)) {
|
||||
AnimatedVisibility(LocalIsConnected.current) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.height(
|
||||
WindowInsets.statusBars.asPaddingValues()
|
||||
.calculateTopPadding()
|
||||
)
|
||||
)
|
||||
}
|
||||
?: "",
|
||||
presence = presenceFromStatus(
|
||||
RevoltAPI.userCache[RevoltAPI.selfId]?.status?.presence,
|
||||
RevoltAPI.userCache[RevoltAPI.selfId]?.online ?: false
|
||||
),
|
||||
userId = RevoltAPI.selfId ?: "",
|
||||
avatar = RevoltAPI.userCache[RevoltAPI.selfId]?.avatar,
|
||||
size = 48.dp,
|
||||
presenceSize = 16.dp,
|
||||
onClick = {
|
||||
onDestinationChanged(ChatRouterDestination.defaultForDMList)
|
||||
},
|
||||
onLongClick = onLongPressAvatar,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(8.dp)
|
||||
.size(48.dp)
|
||||
)
|
||||
UserAvatar(
|
||||
username = RevoltAPI.userCache[RevoltAPI.selfId]?.let {
|
||||
User.resolveDefaultName(
|
||||
it
|
||||
)
|
||||
}
|
||||
?: "",
|
||||
presence = presenceFromStatus(
|
||||
RevoltAPI.userCache[RevoltAPI.selfId]?.status?.presence,
|
||||
RevoltAPI.userCache[RevoltAPI.selfId]?.online ?: false
|
||||
),
|
||||
userId = RevoltAPI.selfId ?: "",
|
||||
avatar = RevoltAPI.userCache[RevoltAPI.selfId]?.avatar,
|
||||
size = 48.dp,
|
||||
presenceSize = 16.dp,
|
||||
onClick = {
|
||||
onDestinationChanged(ChatRouterDestination.defaultForDMList)
|
||||
},
|
||||
onLongClick = onLongPressAvatar,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.size(48.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
|
|
@ -314,12 +340,12 @@ fun ChannelSideDrawer(
|
|||
)
|
||||
val leftIndicatorColour = animateColorAsState(
|
||||
targetValue =
|
||||
if (serverInList.id == currentServer)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else if (serverHasUnread)
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else
|
||||
Color.Transparent,
|
||||
if (serverInList.id == currentServer)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else if (serverHasUnread)
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else
|
||||
Color.Transparent,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
|
|
@ -438,27 +464,30 @@ fun ChannelSideDrawer(
|
|||
}
|
||||
Column(
|
||||
Modifier
|
||||
.clip(
|
||||
MaterialTheme.shapes.extraLarge.copy(
|
||||
bottomEnd = CornerSize(0)
|
||||
)
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
Box(Modifier.height(serverBannerHeight)) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(
|
||||
MaterialTheme.shapes.medium.copy(
|
||||
topStart = CornerSize(0.dp),
|
||||
topEnd = CornerSize(0.dp)
|
||||
)
|
||||
)
|
||||
.height(
|
||||
serverBannerHeight + WindowInsets.statusBars.asPaddingValues()
|
||||
.calculateTopPadding()
|
||||
)
|
||||
//.offset(y = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
|
||||
) {
|
||||
if (server?.banner != null) {
|
||||
RemoteImage(
|
||||
url = "$REVOLT_FILES/banners/${server.banner.id}",
|
||||
description = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.clip(
|
||||
MaterialTheme.shapes.medium.copy(
|
||||
topStart = CornerSize(0), topEnd = CornerSize(0)
|
||||
)
|
||||
)
|
||||
.fillMaxSize()
|
||||
)
|
||||
|
||||
|
|
@ -470,7 +499,7 @@ fun ChannelSideDrawer(
|
|||
drawRect(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
surfaceContainer.copy(alpha = 0.8f),
|
||||
Color.Black.copy(alpha = 0.6f),
|
||||
Color.Transparent
|
||||
),
|
||||
Offset.Zero,
|
||||
|
|
@ -482,63 +511,72 @@ fun ChannelSideDrawer(
|
|||
}
|
||||
|
||||
Row(
|
||||
Modifier.padding(16.dp),
|
||||
Modifier
|
||||
.padding(16.dp)
|
||||
.offset(y = serverInfoOffset),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides
|
||||
if (server?.banner != null) Color.White
|
||||
else LocalContentColor.current
|
||||
) {
|
||||
if (server?.flags has ServerFlags.Official) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = R.drawable.ic_revolt_decagram_24dp
|
||||
),
|
||||
contentDescription = stringResource(
|
||||
R.string.server_flag_official
|
||||
),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
if (server?.flags has ServerFlags.Verified) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = R.drawable.ic_check_decagram_24dp
|
||||
),
|
||||
contentDescription = stringResource(
|
||||
R.string.server_flag_verified
|
||||
),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
Row(
|
||||
Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (server?.flags has ServerFlags.Official) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = R.drawable.ic_revolt_decagram_24dp
|
||||
),
|
||||
contentDescription = stringResource(
|
||||
R.string.server_flag_official
|
||||
),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
if (server?.flags has ServerFlags.Verified) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = R.drawable.ic_check_decagram_24dp
|
||||
),
|
||||
contentDescription = stringResource(
|
||||
R.string.server_flag_verified
|
||||
),
|
||||
tint = LocalContentColor.current,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = when (currentServer) {
|
||||
null -> stringResource(R.string.direct_messages)
|
||||
else -> server?.name ?: stringResource(R.string.unknown)
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = when (currentServer) {
|
||||
null -> stringResource(R.string.direct_messages)
|
||||
else -> server?.name ?: stringResource(R.string.unknown)
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
if (currentServer != null) {
|
||||
IconButton(onClick = {
|
||||
server?.id?.let { srvId -> onShowServerContextSheet(srvId) }
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.menu)
|
||||
)
|
||||
if (currentServer != null) {
|
||||
IconButton(onClick = {
|
||||
server?.id?.let { srvId -> onShowServerContextSheet(srvId) }
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.menu),
|
||||
tint = LocalContentColor.current
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Spacer(Modifier.height(64.dp))
|
||||
}
|
||||
} else {
|
||||
Spacer(Modifier.height(64.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -767,7 +805,8 @@ fun ColumnScope.ServerChannelListRenderer(
|
|||
items(categorisedChannels?.size ?: 0) {
|
||||
when (val channelOrCat = categorisedChannels?.get(it)) {
|
||||
is CategorisedChannelList.Channel -> {
|
||||
ChannelItem(channel = channelOrCat.channel,
|
||||
ChannelItem(
|
||||
channel = channelOrCat.channel,
|
||||
isCurrent = when (currentDestination) {
|
||||
is ChatRouterDestination.Channel -> {
|
||||
currentDestination.channelId == channelOrCat.channel.id
|
||||
|
|
@ -845,7 +884,8 @@ fun ChannelItem(
|
|||
}
|
||||
}
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically,
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 8.dp)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package chat.revolt.composables.screens.chat.molecules
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.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.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.revolt.R
|
||||
import chat.revolt.callbacks.Action
|
||||
import chat.revolt.callbacks.ActionChannel
|
||||
import chat.revolt.composables.screens.chat.StackedUserAvatars
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun JoinVoiceChannelButton(channelId: String, modifier: Modifier = Modifier) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.clickable {
|
||||
scope.launch { ActionChannel.send(Action.OpenVoiceChannelOverlay(channelId)) }
|
||||
}
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
StackedUserAvatars(
|
||||
listOf(
|
||||
"01FHGJ3NPP7XANQQH8C2BE44ZY",
|
||||
"01F1WKM5TK2V6KCZWR6DGBJDTZ",
|
||||
"01EX2NCWQ0CHS3QJF0FEQS1GR4"
|
||||
),
|
||||
size = 24.dp,
|
||||
offset = 12.dp,
|
||||
amount = 3,
|
||||
serverId = null
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.voice_join_offering),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.voice_join_offering_description_other, Integer.MAX_VALUE),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package chat.revolt.composables.screens.voice
|
||||
|
||||
import android.content.Context
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
|
||||
@Composable
|
||||
fun VoiceChannelOverlay(channelId: String, onCollapse: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val windowManager =
|
||||
remember { context.getSystemService(Context.WINDOW_SERVICE) as WindowManager }
|
||||
|
||||
val metrics = DisplayMetrics().apply {
|
||||
@Suppress("DEPRECATION") // We *need* the real metrics here, not the window metrics
|
||||
windowManager.defaultDisplay.getRealMetrics(this)
|
||||
}
|
||||
val (width, height) = with(LocalDensity.current) {
|
||||
Pair(metrics.widthPixels.toDp(), metrics.heightPixels.toDp())
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
onCollapse()
|
||||
},
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
decorFitsSystemWindows = true,
|
||||
dismissOnClickOutside = false,
|
||||
dismissOnBackPress = true
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.requiredSize(width, height)
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically)
|
||||
) {
|
||||
Text(text = "Voice channel overlay for $channelId", textAlign = TextAlign.Center)
|
||||
Button(onClick = {
|
||||
onCollapse()
|
||||
}) {
|
||||
Text("Close")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package chat.revolt.composables.voice
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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 androidx.core.net.toUri
|
||||
import chat.revolt.R
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionStatus
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.accompanist.permissions.shouldShowRationale
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VoicePermissionSwitch(onCancel: () -> Unit, onPermissionGranted: @Composable () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var permissionState = rememberMultiplePermissionsState(
|
||||
listOf(
|
||||
Manifest.permission.RECORD_AUDIO,
|
||||
Manifest.permission.CAMERA
|
||||
),
|
||||
onPermissionsResult = {}
|
||||
)
|
||||
var fullyRevoked = permissionState
|
||||
.revokedPermissions
|
||||
.any { it.status is PermissionStatus.Denied && !it.status.shouldShowRationale }
|
||||
|
||||
if (permissionState.allPermissionsGranted) {
|
||||
onPermissionGranted()
|
||||
} else if (permissionState.shouldShowRationale || fullyRevoked) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onCancel,
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||
sheetGesturesEnabled = false,
|
||||
dragHandle = {}
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 32.dp, vertical = 16.dp)
|
||||
) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.voice_join_permission_rationale_heading),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.voice_join_permission_rationale_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.icn_mic_24dp),
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.voice_join_permission_rationale_permission_mic),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.icn_videocam_24dp),
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.voice_join_permission_rationale_permission_camera),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.voice_join_permission_rationale_assurance),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
if (fullyRevoked) {
|
||||
// Launch settings to allow the user to manually enable permissions
|
||||
val intent =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = "package:${context.packageName}".toUri()
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
permissionState.launchMultiplePermissionRequest()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = stringResource(R.string.voice_join_permission_rationale_cta))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
permissionState.launchMultiplePermissionRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package chat.revolt.composables.voice
|
||||
|
||||
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.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import chat.revolt.R
|
||||
import chat.revolt.api.routes.misc.getRootRoute
|
||||
import chat.revolt.api.routes.voice.joinCall
|
||||
import logcat.LogPriority
|
||||
import logcat.asLog
|
||||
import logcat.logcat
|
||||
|
||||
class VoiceSheetViewModel(private val state: SavedStateHandle) : ViewModel() {
|
||||
private val _channelId = mutableStateOf(state.get<String>("channelId") ?: "")
|
||||
var channelId: String
|
||||
get() = _channelId.value
|
||||
set(value) {
|
||||
_channelId.value = value
|
||||
state["channelId"] = value
|
||||
}
|
||||
|
||||
var voiceLkNode by mutableStateOf("")
|
||||
private val _voiceToken = mutableStateOf(state.get<String>("voiceToken") ?: "")
|
||||
var voiceToken: String
|
||||
get() = _voiceToken.value
|
||||
private set(value) {
|
||||
_voiceToken.value = value
|
||||
state["voiceToken"] = value
|
||||
}
|
||||
|
||||
var errorResource by mutableStateOf<Int?>(null)
|
||||
private set
|
||||
|
||||
suspend fun getVoiceToken() {
|
||||
val root = getRootRoute()
|
||||
val lk = root.features.livekit
|
||||
|
||||
if (lk == null) {
|
||||
logcat(LogPriority.ERROR) {
|
||||
IllegalStateException("LiveKit is not supported by this API version!").asLog()
|
||||
}
|
||||
errorResource = R.string.voice_error_not_supported
|
||||
return
|
||||
}
|
||||
|
||||
if (lk.nodes.isEmpty()) {
|
||||
logcat(LogPriority.ERROR) { IllegalStateException("No LiveKit nodes available!").asLog() }
|
||||
errorResource = R.string.voice_error_no_nodes
|
||||
return
|
||||
}
|
||||
|
||||
val node = lk.nodes.random()
|
||||
try {
|
||||
val joined = joinCall(channelId, node.name)
|
||||
voiceLkNode = joined.url
|
||||
voiceToken = joined.token
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR) { "Could not get LiveKit token\n" + e.asLog() }
|
||||
errorResource = R.string.voice_error_generic
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VoiceSheet(
|
||||
channelId: String,
|
||||
onDisconnect: () -> Unit,
|
||||
viewModel: VoiceSheetViewModel = viewModel()
|
||||
) {
|
||||
LaunchedEffect(channelId) {
|
||||
viewModel.channelId = channelId
|
||||
viewModel.getVoiceToken()
|
||||
}
|
||||
|
||||
// TODO - Voice channels are not supported yet
|
||||
LaunchedEffect(Unit) { onDisconnect() }
|
||||
/*RoomScope(
|
||||
url = viewModel.voiceLkNode,
|
||||
token = viewModel.voiceToken,
|
||||
audio = true,
|
||||
video = false,
|
||||
connect = true,
|
||||
) {
|
||||
val room = RoomLocal.current
|
||||
val trackRefs = rememberTracks()
|
||||
|
||||
Column {
|
||||
LazyColumn(modifier = Modifier.animateContentSize()) {
|
||||
items(trackRefs.size) { index ->
|
||||
VideoTrackView(
|
||||
trackReference = trackRefs[index],
|
||||
modifier = Modifier.fillParentMaxHeight(0.5f)
|
||||
)
|
||||
}
|
||||
item(key = "stats") {
|
||||
Text("status = ${room.state.name}")
|
||||
}
|
||||
item(key = "controls") {
|
||||
Button(onClick = {
|
||||
room.setMicrophoneMute(true)
|
||||
}) {
|
||||
Text("mute 🎤🫷😤")
|
||||
}
|
||||
Button(onClick = {
|
||||
room.setMicrophoneMute(false)
|
||||
}) {
|
||||
Text("unmute 🗣️📢🔥")
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors().copy(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.5f),
|
||||
disabledContentColor = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.5f)
|
||||
),
|
||||
onClick = {
|
||||
room.disconnect()
|
||||
onDisconnect()
|
||||
},
|
||||
) {
|
||||
Text("disconnect")
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
|
@ -10,15 +10,17 @@ import androidx.activity.compose.BackHandler
|
|||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
|
|
@ -38,15 +40,20 @@ import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
|
|||
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.runtime.structuralEqualityPolicy
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
|
@ -70,7 +77,6 @@ import chat.revolt.callbacks.Action
|
|||
import chat.revolt.callbacks.ActionChannel
|
||||
import chat.revolt.composables.chat.DisconnectedNotice
|
||||
import chat.revolt.composables.screens.chat.drawer.ChannelSideDrawer
|
||||
import chat.revolt.composables.screens.voice.VoiceChannelOverlay
|
||||
import chat.revolt.dialogs.NotificationRationaleDialog
|
||||
import chat.revolt.internals.Changelogs
|
||||
import chat.revolt.internals.extensions.zero
|
||||
|
|
@ -261,12 +267,16 @@ class ChatRouterViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val LocalIsConnected = compositionLocalOf(structuralEqualityPolicy()) { false }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChatRouterScreen(
|
||||
topNav: NavController,
|
||||
windowSizeClass: WindowSizeClass,
|
||||
disableBackHandler: Boolean,
|
||||
onNullifiedUser: () -> Unit,
|
||||
onEnterVoiceUI: (String) -> Unit,
|
||||
viewModel: ChatRouterViewModel = hiltViewModel()
|
||||
) {
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
|
|
@ -274,6 +284,8 @@ fun ChatRouterScreen(
|
|||
val context = LocalContext.current
|
||||
val view = LocalView.current
|
||||
|
||||
var drawerWidth by remember { mutableFloatStateOf(0.0f) }
|
||||
|
||||
var showPlatformModDMHint by remember { mutableStateOf(false) }
|
||||
|
||||
var showStatusSheet by remember { mutableStateOf(false) }
|
||||
|
|
@ -302,9 +314,6 @@ fun ChatRouterScreen(
|
|||
|
||||
var useTabletAwareUI by remember { mutableStateOf(false) }
|
||||
|
||||
var voiceChannelOverlay by remember { mutableStateOf(false) }
|
||||
var voiceChannelOverlayChannelId by remember { mutableStateOf("") }
|
||||
|
||||
var showReportUser by remember { mutableStateOf(false) }
|
||||
var reportUserTarget by remember { mutableStateOf("") }
|
||||
|
||||
|
|
@ -444,8 +453,7 @@ fun ChatRouterScreen(
|
|||
}
|
||||
|
||||
is Action.OpenVoiceChannelOverlay -> {
|
||||
voiceChannelOverlayChannelId = action.channelId
|
||||
voiceChannelOverlay = true
|
||||
onEnterVoiceUI(action.channelId)
|
||||
}
|
||||
|
||||
is Action.OpenWebhookSheet -> {
|
||||
|
|
@ -712,12 +720,6 @@ fun ChatRouterScreen(
|
|||
}
|
||||
}
|
||||
|
||||
if (voiceChannelOverlay) {
|
||||
VoiceChannelOverlay(voiceChannelOverlayChannelId) {
|
||||
voiceChannelOverlay = false
|
||||
}
|
||||
}
|
||||
|
||||
val askNotificationsPermission =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (isGranted) {
|
||||
|
|
@ -812,53 +814,11 @@ fun ChatRouterScreen(
|
|||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = RealtimeSocket.disconnectionState == DisconnectionState.Connected
|
||||
CompositionLocalProvider(
|
||||
LocalIsConnected provides (RealtimeSocket.disconnectionState == DisconnectionState.Connected)
|
||||
) {
|
||||
Spacer(Modifier.windowInsetsPadding(WindowInsets.statusBars))
|
||||
}
|
||||
|
||||
if (useTabletAwareUI) {
|
||||
Row {
|
||||
DismissibleDrawerSheet(
|
||||
drawerContainerColor = Color.Transparent,
|
||||
windowInsets = WindowInsets.zero
|
||||
) {
|
||||
Sidebar(
|
||||
viewModel = viewModel,
|
||||
topNav = topNav,
|
||||
currentServer = currentServer,
|
||||
onShowStatusSheet = {
|
||||
showStatusSheet = true
|
||||
},
|
||||
onShowServerContextSheet = {
|
||||
serverContextSheetTarget = it
|
||||
showServerContextSheet = true
|
||||
},
|
||||
onShowAddServerSheet = {
|
||||
showAddServerSheet = true
|
||||
},
|
||||
showSettingsButton = isTouchExplorationEnabled,
|
||||
onOpenSettings = {
|
||||
topNav.navigate("settings")
|
||||
},
|
||||
)
|
||||
}
|
||||
ChannelNavigator(
|
||||
dest = viewModel.currentDestination,
|
||||
topNav = topNav,
|
||||
useDrawer = false,
|
||||
toggleDrawer = {
|
||||
toggleDrawerLambda()
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
var useSidebarGesture by remember { mutableStateOf(true) }
|
||||
DismissibleNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
gesturesEnabled = useSidebarGesture,
|
||||
drawerContent = {
|
||||
if (useTabletAwareUI) {
|
||||
Row {
|
||||
DismissibleDrawerSheet(
|
||||
drawerContainerColor = Color.Transparent,
|
||||
windowInsets = WindowInsets.zero
|
||||
|
|
@ -881,28 +841,104 @@ fun ChatRouterScreen(
|
|||
onOpenSettings = {
|
||||
topNav.navigate("settings")
|
||||
},
|
||||
drawerState = drawerState
|
||||
)
|
||||
}
|
||||
},
|
||||
content = {
|
||||
Row(Modifier.fillMaxSize()) {
|
||||
ChannelNavigator(
|
||||
dest = viewModel.currentDestination,
|
||||
topNav = topNav,
|
||||
useDrawer = true,
|
||||
toggleDrawer = {
|
||||
toggleDrawerLambda()
|
||||
},
|
||||
drawerState = drawerState,
|
||||
drawerGestureEnabled = useSidebarGesture,
|
||||
setDrawerGestureEnabled = {
|
||||
useSidebarGesture = it
|
||||
}
|
||||
)
|
||||
}
|
||||
ChannelNavigator(
|
||||
dest = viewModel.currentDestination,
|
||||
topNav = topNav,
|
||||
useDrawer = false,
|
||||
disableBackHandler = disableBackHandler,
|
||||
toggleDrawer = {
|
||||
toggleDrawerLambda()
|
||||
},
|
||||
onEnterVoiceUI = onEnterVoiceUI,
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
var useSidebarGesture by remember { mutableStateOf(true) }
|
||||
DismissibleNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
gesturesEnabled = useSidebarGesture,
|
||||
drawerContent = {
|
||||
DismissibleDrawerSheet(
|
||||
drawerContainerColor = Color.Transparent,
|
||||
windowInsets = WindowInsets.zero,
|
||||
modifier = Modifier.onSizeChanged {
|
||||
drawerWidth = it.width.toFloat()
|
||||
}
|
||||
) {
|
||||
Sidebar(
|
||||
viewModel = viewModel,
|
||||
topNav = topNav,
|
||||
currentServer = currentServer,
|
||||
onShowStatusSheet = {
|
||||
showStatusSheet = true
|
||||
},
|
||||
onShowServerContextSheet = {
|
||||
serverContextSheetTarget = it
|
||||
showServerContextSheet = true
|
||||
},
|
||||
onShowAddServerSheet = {
|
||||
showAddServerSheet = true
|
||||
},
|
||||
showSettingsButton = isTouchExplorationEnabled,
|
||||
onOpenSettings = {
|
||||
topNav.navigate("settings")
|
||||
},
|
||||
drawerState = drawerState
|
||||
)
|
||||
}
|
||||
},
|
||||
content = {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
ChannelNavigator(
|
||||
dest = viewModel.currentDestination,
|
||||
topNav = topNav,
|
||||
useDrawer = true,
|
||||
disableBackHandler = disableBackHandler,
|
||||
toggleDrawer = {
|
||||
toggleDrawerLambda()
|
||||
},
|
||||
drawerState = drawerState,
|
||||
drawerGestureEnabled = useSidebarGesture,
|
||||
setDrawerGestureEnabled = {
|
||||
useSidebarGesture = it
|
||||
},
|
||||
onEnterVoiceUI = onEnterVoiceUI,
|
||||
)
|
||||
|
||||
// This is the overlay on the main content when the drawer is open
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Box(
|
||||
Modifier
|
||||
.then(
|
||||
if (drawerState.isOpen) {
|
||||
Modifier.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
enabled = drawerState.isOpen,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
MaterialTheme
|
||||
.colorScheme
|
||||
.surfaceContainerLowest
|
||||
.copy(
|
||||
alpha = (1.0f + (drawerState.currentOffset / drawerWidth)) * 0.7f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -942,11 +978,13 @@ fun ChannelNavigator(
|
|||
toggleDrawer: () -> Unit,
|
||||
drawerState: DrawerState? = null,
|
||||
drawerGestureEnabled: Boolean = true,
|
||||
disableBackHandler: Boolean = false,
|
||||
onEnterVoiceUI: (String) -> Unit = {},
|
||||
setDrawerGestureEnabled: (Boolean) -> Unit = {},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
BackHandler(enabled = useDrawer) {
|
||||
BackHandler(useDrawer && !disableBackHandler) {
|
||||
toggleDrawer()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -11,10 +12,14 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
|
@ -97,6 +102,7 @@ import chat.revolt.composables.generic.CountableListHeader
|
|||
import chat.revolt.composables.generic.UserAvatar
|
||||
import chat.revolt.internals.extensions.zero
|
||||
import chat.revolt.markdown.jbm.asHexString
|
||||
import chat.revolt.screens.chat.LocalIsConnected
|
||||
import io.github.g00fy2.quickie.QRResult
|
||||
import io.github.g00fy2.quickie.ScanQRCode
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -470,69 +476,80 @@ fun FriendsScreen(topNav: NavController, useDrawer: Boolean, onDrawerClicked: ()
|
|||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.friends),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
Column {
|
||||
AnimatedVisibility(LocalIsConnected.current) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.height(
|
||||
WindowInsets.statusBars.asPaddingValues()
|
||||
.calculateTopPadding()
|
||||
)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
if (useDrawer) {
|
||||
}
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.friends),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
if (useDrawer) {
|
||||
IconButton(onClick = {
|
||||
onDrawerClicked()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(id = R.string.menu)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
onDrawerClicked()
|
||||
overflowMenuShown = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(id = R.string.menu)
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.menu)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
overflowMenuShown = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.menu)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = overflowMenuShown,
|
||||
onDismissRequest = {
|
||||
overflowMenuShown = false
|
||||
}
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(R.string.friends_deny_all_incoming))
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
overflowMenuShown = false
|
||||
}
|
||||
with(Dispatchers.IO) {
|
||||
DropdownMenu(
|
||||
expanded = overflowMenuShown,
|
||||
onDismissRequest = {
|
||||
overflowMenuShown = false
|
||||
}
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(stringResource(R.string.friends_deny_all_incoming))
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
FriendRequests.getIncoming()
|
||||
.forEach { it.id?.let { id -> unfriendUser(id) } }
|
||||
overflowMenuShown = false
|
||||
}
|
||||
with(Dispatchers.IO) {
|
||||
scope.launch {
|
||||
FriendRequests.getIncoming()
|
||||
.forEach { it.id?.let { id -> unfriendUser(id) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.zero
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.zero
|
||||
)
|
||||
}
|
||||
},
|
||||
) { pv ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(pv)
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
LazyColumn(state = listState) {
|
||||
LazyColumn {
|
||||
stickyHeader(key = "incoming") {
|
||||
CountableListHeader(
|
||||
text = stringResource(id = R.string.friends_incoming_requests),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
package chat.revolt.screens.chat.views
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
|
@ -23,28 +28,40 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import chat.revolt.R
|
||||
import chat.revolt.internals.extensions.zero
|
||||
import chat.revolt.screens.chat.LocalIsConnected
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NoCurrentChannelScreen(useDrawer: Boolean, onDrawerClicked: () -> Unit) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
if (useDrawer) {
|
||||
IconButton(onClick = {
|
||||
onDrawerClicked()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(id = R.string.menu)
|
||||
Column {
|
||||
AnimatedVisibility(LocalIsConnected.current) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.height(
|
||||
WindowInsets.statusBars.asPaddingValues()
|
||||
.calculateTopPadding()
|
||||
)
|
||||
)
|
||||
}
|
||||
TopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
if (useDrawer) {
|
||||
IconButton(onClick = {
|
||||
onDrawerClicked()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(id = R.string.menu)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.zero
|
||||
)
|
||||
},
|
||||
windowInsets = WindowInsets.zero
|
||||
)
|
||||
}
|
||||
},
|
||||
) { pv ->
|
||||
Column(
|
||||
|
|
|
|||
|
|
@ -13,11 +13,13 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.exclude
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -62,6 +64,7 @@ import chat.revolt.composables.generic.NonIdealState
|
|||
import chat.revolt.composables.screens.settings.UserOverview
|
||||
import chat.revolt.composables.skeletons.UserOverviewSkeleton
|
||||
import chat.revolt.internals.extensions.zero
|
||||
import chat.revolt.screens.chat.LocalIsConnected
|
||||
import chat.revolt.sheets.UserCardSheet
|
||||
import io.sentry.Sentry
|
||||
|
||||
|
|
@ -110,22 +113,33 @@ fun OverviewScreen(
|
|||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text(stringResource(R.string.overview_screen_title)) },
|
||||
navigationIcon = {
|
||||
if (useDrawer) {
|
||||
IconButton(onClick = {
|
||||
onDrawerClicked()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(id = R.string.menu)
|
||||
Column {
|
||||
AnimatedVisibility(LocalIsConnected.current) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.height(
|
||||
WindowInsets.statusBars.asPaddingValues()
|
||||
.calculateTopPadding()
|
||||
)
|
||||
)
|
||||
}
|
||||
CenterAlignedTopAppBar(
|
||||
title = { Text(stringResource(R.string.overview_screen_title)) },
|
||||
navigationIcon = {
|
||||
if (useDrawer) {
|
||||
IconButton(onClick = {
|
||||
onDrawerClicked()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(id = R.string.menu)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.zero
|
||||
)
|
||||
},
|
||||
windowInsets = WindowInsets.zero
|
||||
)
|
||||
}
|
||||
},
|
||||
contentWindowInsets = if (includePadding) ScaffoldDefaults.contentWindowInsets else ScaffoldDefaults.contentWindowInsets.exclude(
|
||||
NavigationBarDefaults.windowInsets
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
|
@ -91,6 +92,7 @@ import androidx.compose.runtime.snapshotFlow
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
|
@ -122,6 +124,7 @@ import chat.revolt.api.routes.channel.react
|
|||
import chat.revolt.api.routes.microservices.autumn.FileArgs
|
||||
import chat.revolt.api.schemas.ChannelType
|
||||
import chat.revolt.api.schemas.Message
|
||||
import chat.revolt.api.settings.FeatureFlags
|
||||
import chat.revolt.callbacks.Action
|
||||
import chat.revolt.callbacks.ActionChannel
|
||||
import chat.revolt.composables.chat.DateDivider
|
||||
|
|
@ -140,10 +143,12 @@ import chat.revolt.composables.screens.chat.ChannelIcon
|
|||
import chat.revolt.composables.screens.chat.ReplyManager
|
||||
import chat.revolt.composables.screens.chat.TypingIndicator
|
||||
import chat.revolt.composables.screens.chat.atoms.RegularMessage
|
||||
import chat.revolt.composables.screens.chat.molecules.JoinVoiceChannelButton
|
||||
import chat.revolt.composables.skeletons.MessageSkeleton
|
||||
import chat.revolt.composables.skeletons.MessageSkeletonVariant
|
||||
import chat.revolt.internals.extensions.rememberChannelPermissions
|
||||
import chat.revolt.internals.extensions.zero
|
||||
import chat.revolt.screens.chat.LocalIsConnected
|
||||
import chat.revolt.sheets.ChannelInfoSheet
|
||||
import chat.revolt.sheets.MessageContextSheet
|
||||
import chat.revolt.sheets.ReactSheet
|
||||
|
|
@ -153,7 +158,6 @@ import com.valentinilk.shimmer.shimmer
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Instant
|
||||
import logcat.logcat
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
|
||||
|
|
@ -512,126 +516,137 @@ fun ChannelScreen(
|
|||
Scaffold(
|
||||
contentWindowInsets = WindowInsets.zero,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier.clickable {
|
||||
channelInfoSheetShown = true
|
||||
},
|
||||
title = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
viewModel.channel?.let {
|
||||
when (it.channelType) {
|
||||
ChannelType.DirectMessage -> {
|
||||
Column {
|
||||
AnimatedVisibility(LocalIsConnected.current) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.height(
|
||||
WindowInsets.statusBars.asPaddingValues()
|
||||
.calculateTopPadding()
|
||||
)
|
||||
)
|
||||
}
|
||||
TopAppBar(
|
||||
modifier = Modifier.clickable {
|
||||
channelInfoSheetShown = true
|
||||
},
|
||||
title = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
viewModel.channel?.let {
|
||||
when (it.channelType) {
|
||||
ChannelType.DirectMessage -> {
|
||||
val partner =
|
||||
RevoltAPI.userCache[ChannelUtils.resolveDMPartner(it)]
|
||||
UserAvatar(
|
||||
username = it.name ?: stringResource(R.string.unknown),
|
||||
userId = ChannelUtils.resolveDMPartner(it) ?: "",
|
||||
size = 24.dp,
|
||||
presenceSize = 12.dp,
|
||||
avatar = partner?.avatar
|
||||
)
|
||||
}
|
||||
|
||||
ChannelType.Group -> {
|
||||
GroupIcon(
|
||||
name = it.name ?: stringResource(R.string.unknown),
|
||||
size = 24.dp,
|
||||
icon = it.icon
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
ChannelIcon(
|
||||
channelType = it.channelType ?: ChannelType.TextChannel,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.alpha(0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides LocalTextStyle.current.copy(
|
||||
fontSize = 20.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Bottom,
|
||||
trim = LineHeightStyle.Trim.LastLineBottom
|
||||
)
|
||||
)
|
||||
) {
|
||||
when (it.channelType) {
|
||||
ChannelType.TextChannel, ChannelType.VoiceChannel, ChannelType.Group -> Text(
|
||||
it.name ?: stringResource(R.string.unknown),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
ChannelType.SavedMessages -> Text(
|
||||
stringResource(R.string.channel_notes),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
ChannelType.DirectMessage -> Text(
|
||||
ChannelUtils.resolveName(it)
|
||||
?: stringResource(R.string.unknown),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
else -> Text(
|
||||
stringResource(R.string.unknown),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (it.channelType == ChannelType.DirectMessage) {
|
||||
val partner =
|
||||
RevoltAPI.userCache[ChannelUtils.resolveDMPartner(it)]
|
||||
UserAvatar(
|
||||
username = it.name ?: stringResource(R.string.unknown),
|
||||
userId = ChannelUtils.resolveDMPartner(it) ?: "",
|
||||
size = 24.dp,
|
||||
presenceSize = 12.dp,
|
||||
avatar = partner?.avatar
|
||||
PresenceBadge(
|
||||
presence = presenceFromStatus(
|
||||
partner?.status?.presence,
|
||||
online = partner?.online == true
|
||||
),
|
||||
size = 12.dp
|
||||
)
|
||||
}
|
||||
|
||||
ChannelType.Group -> {
|
||||
GroupIcon(
|
||||
name = it.name ?: stringResource(R.string.unknown),
|
||||
size = 24.dp,
|
||||
icon = it.icon
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
ChannelIcon(
|
||||
channelType = it.channelType ?: ChannelType.TextChannel,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.alpha(0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides LocalTextStyle.current.copy(
|
||||
fontSize = 20.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Bottom,
|
||||
trim = LineHeightStyle.Trim.LastLineBottom
|
||||
)
|
||||
)
|
||||
) {
|
||||
when (it.channelType) {
|
||||
ChannelType.TextChannel, ChannelType.VoiceChannel, ChannelType.Group -> Text(
|
||||
it.name ?: stringResource(R.string.unknown),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
ChannelType.SavedMessages -> Text(
|
||||
stringResource(R.string.channel_notes),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
ChannelType.DirectMessage -> Text(
|
||||
ChannelUtils.resolveName(it)
|
||||
?: stringResource(R.string.unknown),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
else -> Text(
|
||||
stringResource(R.string.unknown),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (it.channelType == ChannelType.DirectMessage) {
|
||||
val partner =
|
||||
RevoltAPI.userCache[ChannelUtils.resolveDMPartner(it)]
|
||||
PresenceBadge(
|
||||
presence = presenceFromStatus(
|
||||
partner?.status?.presence,
|
||||
online = partner?.online == true
|
||||
),
|
||||
size = 12.dp
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.alpha(0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
windowInsets = if (useChatUI) WindowInsets.statusBars else WindowInsets.zero,
|
||||
navigationIcon = {
|
||||
if (useDrawer) {
|
||||
IconButton(onClick = onToggleDrawer) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(id = R.string.menu)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (useBackButton) {
|
||||
IconButton(onClick = backButtonAction ?: {}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.back)
|
||||
)
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.alpha(0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
windowInsets = if (useChatUI) WindowInsets.statusBars else WindowInsets.zero,
|
||||
navigationIcon = {
|
||||
if (useDrawer) {
|
||||
IconButton(onClick = onToggleDrawer) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(id = R.string.menu)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (useBackButton) {
|
||||
IconButton(onClick = backButtonAction ?: {}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.back)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
) { pv ->
|
||||
Crossfade(
|
||||
|
|
@ -847,74 +862,85 @@ fun ChannelScreen(
|
|||
}
|
||||
}
|
||||
|
||||
if (viewModel.showPhysicalKeyboardSpark) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.spark_keyboard_shortcuts),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
val raw =
|
||||
stringResource(R.string.spark_keyboard_shortcuts_description)
|
||||
val before = raw.substringBefore("%1\$s")
|
||||
val after = raw.substringAfter("%1\$s")
|
||||
|
||||
append(before)
|
||||
appendInlineContent("metaKey", "Meta")
|
||||
append(" + /")
|
||||
append(after)
|
||||
},
|
||||
inlineContent = mapOf(
|
||||
"metaKey" to InlineTextContent(
|
||||
placeholder = Placeholder(
|
||||
width = 1.em,
|
||||
height = 1.em,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||
)
|
||||
) {
|
||||
with(LocalDensity.current) {
|
||||
Image(
|
||||
painterResource(R.drawable.ic_meta_key_24dp),
|
||||
contentDescription = null,
|
||||
/*modifier = Modifier.size(1.em.toDp())*/
|
||||
)
|
||||
}
|
||||
}
|
||||
),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
if (viewModel.showPhysicalKeyboardSpark) {
|
||||
Card {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.dismissPhysicalKeyboardSpark()
|
||||
Text(
|
||||
stringResource(R.string.spark_keyboard_shortcuts),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
val raw =
|
||||
stringResource(R.string.spark_keyboard_shortcuts_description)
|
||||
val before = raw.substringBefore("%1\$s")
|
||||
val after = raw.substringAfter("%1\$s")
|
||||
|
||||
append(before)
|
||||
appendInlineContent("metaKey", "Meta")
|
||||
append(" + /")
|
||||
append(after)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
inlineContent = mapOf(
|
||||
"metaKey" to InlineTextContent(
|
||||
placeholder = Placeholder(
|
||||
width = 1.em,
|
||||
height = 1.em,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||
)
|
||||
) {
|
||||
with(LocalDensity.current) {
|
||||
Image(
|
||||
painterResource(R.drawable.ic_meta_key_24dp),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(
|
||||
LocalContentColor.current
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.spark_keyboard_shortcuts_dismiss))
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
(context as Activity).requestShowKeyboardShortcuts()
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.spark_keyboard_shortcuts_cta))
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.dismissPhysicalKeyboardSpark()
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.spark_keyboard_shortcuts_dismiss))
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
(context as Activity).requestShowKeyboardShortcuts()
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.spark_keyboard_shortcuts_cta))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.channel?.channelType == ChannelType.VoiceChannel
|
||||
&& FeatureFlags.voiceChannels2_0Granted
|
||||
) {
|
||||
JoinVoiceChannelButton(channelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -960,7 +986,7 @@ fun ChannelScreen(
|
|||
onToggleSpoiler = {
|
||||
val index = viewModel.draftAttachments
|
||||
.indexOfFirst { a -> a.pickerIdentifier == it.pickerIdentifier }
|
||||
|
||||
|
||||
if (index != -1) {
|
||||
val attachment =
|
||||
viewModel.draftAttachments[index]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateListOf
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.util.fastDistinctBy
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import chat.revolt.R
|
||||
|
|
@ -876,7 +877,14 @@ class ChannelScreenViewModel @Inject constructor(
|
|||
|
||||
withContext(Dispatchers.Main) {
|
||||
items.clear()
|
||||
items.addAll(groupedItems)
|
||||
items.addAll(groupedItems.fastDistinctBy {
|
||||
when (it) {
|
||||
is ChannelScreenItem.RegularMessage -> it.message.id
|
||||
is ChannelScreenItem.SystemMessage -> it.message.id
|
||||
is ChannelScreenItem.DateDivider -> it.instant.toString()
|
||||
else -> it.toString() // Fallback for other item types
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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="M360,360L360,280L720,280L720,360L360,360ZM360,480L360,400L720,400L720,480L360,480ZM480,800L240,800Q223,800 211.5,800Q200,800 200,800L200,800L480,800L480,800ZM480,880L240,880Q190,880 155,845Q120,810 120,760L120,640L240,640L240,80L840,80L840,441Q820,439 799.5,442.5Q779,446 760,455L760,160L320,160L320,640L560,640L480,720L200,720L200,760Q200,777 211.5,788.5Q223,800 240,800L480,800L480,880ZM560,880L560,757L781,537Q790,528 801,524Q812,520 823,520Q835,520 846,524.5Q857,529 866,538L903,575Q911,584 915.5,595Q920,606 920,617Q920,628 916,639.5Q912,651 903,660L683,880L560,880ZM860,617L860,617L823,580L823,580L860,617ZM620,820L658,820L779,698L761,679L742,661L620,782L620,820ZM761,679L742,661L742,661L779,698L779,698L761,679Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="M440,520Q457,520 468.5,508.5Q480,497 480,480Q480,463 468.5,451.5Q457,440 440,440Q423,440 411.5,451.5Q400,463 400,480Q400,497 411.5,508.5Q423,520 440,520ZM280,840L280,760L520,720L520,275Q520,260 511,248Q502,236 488,234L280,200L280,120L500,156Q544,164 572,197Q600,230 600,274L600,786L280,840ZM120,840L120,760L200,760L200,200Q200,166 223.5,143Q247,120 280,120L680,120Q714,120 737,143Q760,166 760,200L760,760L840,760L840,840L120,840ZM280,760L680,760L680,200Q680,200 680,200Q680,200 680,200L280,200Q280,200 280,200Q280,200 280,200L280,760Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="M160,840L160,760L640,760L640,840L160,840ZM386,646L160,420L244,334L472,560L386,646ZM640,392L414,164L500,80L726,306L640,392ZM824,800L302,278L358,222L880,744L824,800Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="M500,478Q529,446 544.5,405Q560,364 560,320Q560,276 544.5,235Q529,194 500,162Q500,162 500,162Q500,162 500,162Q560,170 600,215Q640,260 640,320Q640,380 600,425Q560,470 500,478Q500,478 500,478Q500,478 500,478ZM720,800L720,680Q720,644 704,611.5Q688,579 662,554Q713,572 756.5,600.5Q800,629 800,680L800,800L720,800ZM800,520L800,440L720,440L720,360L800,360L800,280L880,280L880,360L960,360L960,440L880,440L880,520L800,520ZM320,480Q254,480 207,433Q160,386 160,320Q160,254 207,207Q254,160 320,160Q386,160 433,207Q480,254 480,320Q480,386 433,433Q386,480 320,480ZM0,800L0,688Q0,654 17.5,625.5Q35,597 64,582Q126,551 190,535.5Q254,520 320,520Q386,520 450,535.5Q514,551 576,582Q605,597 622.5,625.5Q640,654 640,688L640,800L0,800ZM320,400Q353,400 376.5,376.5Q400,353 400,320Q400,287 376.5,263.5Q353,240 320,240Q287,240 263.5,263.5Q240,287 240,320Q240,353 263.5,376.5Q287,400 320,400ZM80,720L560,720L560,688Q560,677 554.5,668Q549,659 540,654Q486,627 431,613.5Q376,600 320,600Q264,600 209,613.5Q154,627 100,654Q91,659 85.5,668Q80,677 80,688L80,720ZM320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320ZM320,720L320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720L320,720Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="M500,478Q529,446 544.5,405Q560,364 560,320Q560,276 544.5,235Q529,194 500,162Q500,162 500,162Q500,162 500,162Q560,170 600,215Q640,260 640,320Q640,380 600,425Q560,470 500,478Q500,478 500,478Q500,478 500,478ZM720,800L720,680Q720,644 704,611.5Q688,579 662,554Q713,572 756.5,600.5Q800,629 800,680L800,800L720,800ZM960,440L720,440L720,360L960,360L960,440ZM320,480Q254,480 207,433Q160,386 160,320Q160,254 207,207Q254,160 320,160Q386,160 433,207Q480,254 480,320Q480,386 433,433Q386,480 320,480ZM0,800L0,688Q0,654 17.5,625.5Q35,597 64,582Q126,551 190,535.5Q254,520 320,520Q386,520 450,535.5Q514,551 576,582Q605,597 622.5,625.5Q640,654 640,688L640,800L0,800ZM320,400Q353,400 376.5,376.5Q400,353 400,320Q400,287 376.5,263.5Q353,240 320,240Q287,240 263.5,263.5Q240,287 240,320Q240,353 263.5,376.5Q287,400 320,400ZM80,720L560,720L560,688Q560,677 554.5,668Q549,659 540,654Q486,627 431,613.5Q376,600 320,600Q264,600 209,613.5Q154,627 100,654Q91,659 85.5,668Q80,677 80,688L80,720ZM320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320ZM320,720L320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720L320,720Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="M440,680L520,680L520,440L440,440L440,680ZM480,360Q497,360 508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<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"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M551,160L280,160L280,80L631,80L551,160ZM391,320L160,320L160,240L471,240L391,320ZM231,480L40,480L40,400L311,400L231,480ZM584,560L480,456L280,656Q280,656 280,656Q280,656 280,656L384,760Q384,760 384,760Q384,760 384,760L584,560ZM537,399L641,503L840,304Q840,304 840,304Q840,304 840,304L736,200Q736,200 736,200Q736,200 736,200L537,399ZM453,371L669,587L440,816Q416,840 384,840Q352,840 328,816L326,814L300,840L100,840L226,714L224,712Q200,688 200,656Q200,624 224,600L453,371ZM453,371L680,144Q704,120 736,120Q768,120 792,144L896,248Q920,272 920,304Q920,336 896,360L669,587L453,371Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="M280,560Q247,560 223.5,536.5Q200,513 200,480Q200,447 223.5,423.5Q247,400 280,400Q313,400 336.5,423.5Q360,447 360,480Q360,513 336.5,536.5Q313,560 280,560ZM280,720Q180,720 110,650Q40,580 40,480Q40,380 110,310Q180,240 280,240Q347,240 401.5,273Q456,306 488,360L840,360L960,480L780,660L700,600L620,660L535,600L488,600Q456,654 401.5,687Q347,720 280,720ZM280,640Q336,640 378.5,606Q421,572 435,520L560,520L618,561L700,500L771,555L846,480L806,440L435,440Q421,388 378.5,354Q336,320 280,320Q214,320 167,367Q120,414 120,480Q120,546 167,593Q214,640 280,640Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="M363,680L363,680L363,680Q363,680 363,680Q363,680 363,680L363,680Q363,680 363,680Q363,680 363,680Q363,680 363,680Q363,680 363,680L363,680Q363,680 363,680Q363,680 363,680Q363,680 363,680Q363,680 363,680L363,680Q363,680 363,680Q363,680 363,680L363,680ZM46,800L184,524Q194,504 212.5,492Q231,480 254,480Q278,480 298,492.5Q318,505 327,528L354,594Q356,600 363,599.5Q370,599 372,593L458,306Q472,258 511.5,229Q551,200 601,200Q650,200 688.5,228.5Q727,257 742,303L778,407Q763,412 749.5,420Q736,428 724,440L710,455L666,328Q658,305 641,292.5Q624,280 601,280Q578,280 560.5,293Q543,306 535,329L449,616Q440,644 416.5,662Q393,680 363,680Q336,680 313,665.5Q290,651 280,625L253,559Q253,559 253,559Q253,559 253,559L135,800L46,800ZM560,840L560,717L781,497Q790,488 801,484Q812,480 823,480Q835,480 846,484.5Q857,489 866,498L903,535Q911,544 915.5,555Q920,566 920,577Q920,588 916,599.5Q912,611 903,620L683,840L560,840ZM860,577L860,577L823,540L823,540L860,577ZM620,780L658,780L779,658L761,639L742,621L620,742L620,780ZM761,639L742,621L742,621L779,658L779,658L761,639ZM240,400Q190,400 155,364.5Q120,329 120,280Q120,230 155,195Q190,160 240,160Q290,160 325,195Q360,230 360,280Q360,329 325,364.5Q290,400 240,400ZM240,320Q257,320 268.5,308.5Q280,297 280,280Q280,263 268.5,251.5Q257,240 240,240Q223,240 211.5,251.5Q200,263 200,280Q200,297 211.5,308.5Q223,320 240,320ZM240,280Q240,280 240,280Q240,280 240,280Q240,280 240,280Q240,280 240,280Q240,280 240,280Q240,280 240,280Q240,280 240,280Q240,280 240,280Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="M480,560Q430,560 395,525Q360,490 360,440L360,200Q360,150 395,115Q430,80 480,80Q530,80 565,115Q600,150 600,200L600,440Q600,490 565,525Q530,560 480,560ZM480,320Q480,320 480,320Q480,320 480,320L480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320L480,320Q480,320 480,320Q480,320 480,320ZM440,840L440,717Q336,703 268,624Q200,545 200,440L280,440Q280,523 338.5,581.5Q397,640 480,640Q563,640 621.5,581.5Q680,523 680,440L760,440Q760,545 692,624Q624,703 520,717L520,840L440,840ZM480,480Q497,480 508.5,468.5Q520,457 520,440L520,200Q520,183 508.5,171.5Q497,160 480,160Q463,160 451.5,171.5Q440,183 440,200L440,440Q440,457 451.5,468.5Q463,480 480,480Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="M414,792L426,736Q429,723 438.5,714.5Q448,706 462,704L586,694Q599,692 610,699Q621,706 626,718L642,756Q681,733 712,700.5Q743,668 764,628L752,622Q741,614 736,602.5Q731,591 734,578L762,456Q765,444 774.5,436Q784,428 796,426Q791,401 783.5,377.5Q776,354 764,332Q755,337 744.5,336.5Q734,336 726,330L620,266Q609,259 604,247Q599,235 602,222L610,188Q579,174 546.5,167Q514,160 480,160Q466,160 451,161.5Q436,163 422,166L452,234Q457,246 454.5,259Q452,272 442,280L348,362Q338,371 324.5,372Q311,373 300,366L208,310Q185,348 172.5,391.5Q160,435 160,480Q160,496 164,532L252,524Q266,522 277.5,528.5Q289,535 294,548L342,662Q347,674 344.5,687Q342,700 332,708L294,740Q321,760 351.5,773Q382,786 414,792ZM486,620Q473,622 462,615Q451,608 446,596L392,472Q387,460 390.5,447Q394,434 404,426L506,340Q515,331 528,330Q541,329 552,336L664,402Q675,409 681,421Q687,433 684,446L652,576Q649,589 640,597.5Q631,606 618,608L486,620ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<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"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L640,160Q673,160 696.5,183.5Q720,207 720,240L720,420L880,260L880,700L720,540L720,720Q720,753 696.5,776.5Q673,800 640,800L160,800ZM160,720L640,720Q640,720 640,720Q640,720 640,720L640,240Q640,240 640,240Q640,240 640,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720Z"/>
|
||||
</vector>
|
||||
|
|
@ -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="M430,460L713,177Q725,165 741,165Q757,165 769,177Q781,189 781,205Q781,221 769,233L487,516L430,460ZM529,559L783,304Q795,292 811.5,292Q828,292 840,304Q852,316 852,332.5Q852,349 840,361L586,615L529,559ZM211,749Q120,658 120,530Q120,402 211,311L331,191L390,250Q397,257 402,264.5Q407,272 412,280L560,131Q572,119 588.5,119Q605,119 617,131Q629,143 629,159.5Q629,176 617,188L444,361L444,361L359,445L378,464Q424,510 422,574Q420,638 373,685L373,685L316,629L316,629Q339,606 341.5,574.5Q344,543 321,520L274,474Q262,462 262,445.5Q262,429 274,417L331,361Q343,349 343,332.5Q343,316 331,304L331,304L267,368Q199,436 199,530.5Q199,625 267,693Q335,761 430,761Q525,761 593,693L832,453Q844,441 860.5,441Q877,441 889,453Q901,465 901,481.5Q901,498 889,510L649,749Q558,840 430,840Q302,840 211,749ZM430,530Q430,530 430,530Q430,530 430,530L430,530L430,530L430,530Q430,530 430,530Q430,530 430,530L430,530L430,530L430,530Q430,530 430,530Q430,530 430,530L430,530Q430,530 430,530Q430,530 430,530ZM680,921L680,840Q746,840 793,793Q840,746 840,680L921,680Q921,780 850.5,850.5Q780,921 680,921ZM39,280Q39,180 109.5,109.5Q180,39 280,39L280,120Q214,120 167,167Q120,214 120,280L39,280Z"/>
|
||||
</vector>
|
||||
|
|
@ -610,6 +610,21 @@
|
|||
<string name="emoji_picker_search_results_header">Search results</string>
|
||||
<string name="emoji_picker_clear_search">Clear search</string>
|
||||
|
||||
<string name="voice_join_offering">Join the voice channel</string>
|
||||
<string name="voice_join_offering_description_zero">Start the call</string>
|
||||
<string name="voice_join_offering_description_one">with %1$s</string>
|
||||
<string name="voice_join_offering_description_other">with %1$d others</string>
|
||||
<string name="voice_join_permission_rationale_heading">We need your permission</string>
|
||||
<string name="voice_join_permission_rationale_description">To join a voice channel, you need to grant the following permissions:</string>
|
||||
<string name="voice_join_permission_rationale_permission_mic">Your microphone, so others can hear you when unmuted</string>
|
||||
<string name="voice_join_permission_rationale_permission_camera">Your camera, so others can see you if you want to share video</string>
|
||||
<string name="voice_join_permission_rationale_assurance">Don\'t worry, we won\'t use your microphone or camera without your permission.</string>
|
||||
<string name="voice_join_permission_rationale_cta">Grant permissions</string>
|
||||
|
||||
<string name="voice_error_not_supported">Voice channels are not available at the moment.</string>
|
||||
<string name="voice_error_no_nodes">All voice nodes are unavailable at the moment. Please try again later.</string>
|
||||
<string name="voice_error_generic">An error occurred. Please try again later.</string>
|
||||
|
||||
<string name="spark_notifications_rationale">Stay in the loop</string>
|
||||
<string name="spark_notifications_rationale_description">Enable notifications to be kept up to date with messages and mentions.</string>
|
||||
<string name="spark_notifications_rationale_cta">Enable notifications</string>
|
||||
|
|
@ -769,7 +784,7 @@
|
|||
<string name="share_target_invalid_intent">This is not a valid share intent.</string>
|
||||
<string name="share_target_attachment_too_large">This attachment is too large for Revolt (max. $1$s).</string>
|
||||
<string name="share_target_search_channels">Search channels</string>
|
||||
<string name="share_target_select_channel">m select a channel to share to.</string>
|
||||
<string name="share_target_select_channel">Select a channel to share to.</string>
|
||||
|
||||
<string name="notification_channel_group_conversations">Conversations</string>
|
||||
<string name="notification_channel_group_social">Friends and Social</string>
|
||||
|
|
|
|||
Loading…
Reference in New Issue