feat: permission management for voice chat
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
2b752fefbc
commit
afba6f9ecf
|
|
@ -48,10 +48,12 @@ 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
|
||||
|
|
@ -60,6 +62,7 @@ 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
|
||||
|
|
@ -84,6 +87,7 @@ 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.material.EasingTokens
|
||||
import chat.revolt.ndk.NativeLibraries
|
||||
import chat.revolt.persistence.KVStorage
|
||||
|
|
@ -433,7 +437,9 @@ fun AppEntrypoint(
|
|||
onRetryConnection: () -> Unit,
|
||||
onUpdateNextDestination: (String) -> Unit = {}
|
||||
) {
|
||||
var showVoiceUI by remember { mutableStateOf(false) }
|
||||
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(
|
||||
|
|
@ -453,6 +459,11 @@ fun AppEntrypoint(
|
|||
showVoiceUI = false
|
||||
}
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(showVoiceUI) {
|
||||
if (showVoiceUI) keyboardController?.hide()
|
||||
}
|
||||
|
||||
val navController = rememberNavController()
|
||||
|
||||
RevoltTheme(
|
||||
|
|
@ -612,8 +623,9 @@ fun AppEntrypoint(
|
|||
)
|
||||
navController.navigate("default")
|
||||
},
|
||||
onEnterVoiceUI = {
|
||||
onEnterVoiceUI = { channelId ->
|
||||
showVoiceUI = true
|
||||
voiceChannelID = channelId
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -752,10 +764,16 @@ fun AppEntrypoint(
|
|||
.widthIn(max = 600.dp)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Button(onClick = {
|
||||
showVoiceUI = false
|
||||
}) {
|
||||
Text("Close voice UI")
|
||||
VoicePermissionSwitch(
|
||||
onCancel = {
|
||||
showVoiceUI = false
|
||||
}
|
||||
) {
|
||||
Button(onClick = {
|
||||
showVoiceUI = false
|
||||
}) {
|
||||
Text("Close voice UI")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,8 @@
|
|||
package chat.revolt.composables.voice
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun VoiceSheet(channelId: String) {
|
||||
|
||||
}
|
||||
|
|
@ -77,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
|
||||
|
|
@ -277,7 +276,7 @@ fun ChatRouterScreen(
|
|||
windowSizeClass: WindowSizeClass,
|
||||
disableBackHandler: Boolean,
|
||||
onNullifiedUser: () -> Unit,
|
||||
onEnterVoiceUI: () -> Unit,
|
||||
onEnterVoiceUI: (String) -> Unit,
|
||||
viewModel: ChatRouterViewModel = hiltViewModel()
|
||||
) {
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
|
|
@ -315,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("") }
|
||||
|
||||
|
|
@ -457,8 +453,7 @@ fun ChatRouterScreen(
|
|||
}
|
||||
|
||||
is Action.OpenVoiceChannelOverlay -> {
|
||||
voiceChannelOverlayChannelId = action.channelId
|
||||
voiceChannelOverlay = true
|
||||
onEnterVoiceUI(action.channelId)
|
||||
}
|
||||
|
||||
is Action.OpenWebhookSheet -> {
|
||||
|
|
@ -725,12 +720,6 @@ fun ChatRouterScreen(
|
|||
}
|
||||
}
|
||||
|
||||
if (voiceChannelOverlay) {
|
||||
VoiceChannelOverlay(voiceChannelOverlayChannelId) {
|
||||
voiceChannelOverlay = false
|
||||
}
|
||||
}
|
||||
|
||||
val askNotificationsPermission =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (isGranted) {
|
||||
|
|
@ -990,7 +979,7 @@ fun ChannelNavigator(
|
|||
drawerState: DrawerState? = null,
|
||||
drawerGestureEnabled: Boolean = true,
|
||||
disableBackHandler: Boolean = false,
|
||||
onEnterVoiceUI: () -> Unit = {},
|
||||
onEnterVoiceUI: (String) -> Unit = {},
|
||||
setDrawerGestureEnabled: (Boolean) -> Unit = {},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
@ -1033,7 +1022,6 @@ fun ChannelNavigator(
|
|||
drawerGestureEnabled = drawerGestureEnabled,
|
||||
setDrawerGestureEnabled = setDrawerGestureEnabled,
|
||||
drawerIsOpen = drawerState?.isOpen == true,
|
||||
onEnterVoiceUI = onEnterVoiceUI,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ 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
|
||||
|
|
@ -201,7 +202,6 @@ fun ChannelScreen(
|
|||
drawerIsOpen: Boolean = false,
|
||||
backButtonAction: (() -> Unit)? = null,
|
||||
useChatUI: Boolean = false,
|
||||
onEnterVoiceUI: () -> Unit = {},
|
||||
viewModel: ChannelScreenViewModel = hiltViewModel()
|
||||
) {
|
||||
// <editor-fold desc="State and effects">
|
||||
|
|
@ -936,15 +936,7 @@ fun ChannelScreen(
|
|||
}
|
||||
|
||||
if (viewModel.channel?.channelType == ChannelType.VoiceChannel) {
|
||||
Button(
|
||||
onClick = {
|
||||
onEnterVoiceUI()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text("Join Voice Channel")
|
||||
}
|
||||
JoinVoiceChannelButton(channelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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>
|
||||
|
|
@ -593,6 +593,17 @@
|
|||
<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="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>
|
||||
|
|
@ -752,7 +763,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