feat: add feedback dialog
This commit is contained in:
parent
9f6d184d18
commit
576efa01ad
|
|
@ -64,10 +64,14 @@ android {
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
buildConfigField "String", "SENTRY_DSN", "\"${property('revoltbuild.properties', 'sentry.dsn', 'RVX_SENTRY_DSN')}\""
|
buildConfigField "String", "SENTRY_DSN", "\"${property('revoltbuild.properties', 'sentry.dsn', 'RVX_SENTRY_DSN')}\""
|
||||||
|
buildConfigField "Boolean", "ANALYSIS_ENABLED", "${property('revoltbuild.properties', 'analysis.enabled', 'RVX_ANALYSIS_ENABLED')}"
|
||||||
|
buildConfigField "String", "ANALYSIS_BASEURL", "\"${property('revoltbuild.properties', 'analysis.base_url', 'RVX_ANALYSIS_BASEURL')}\""
|
||||||
}
|
}
|
||||||
|
|
||||||
debug {
|
debug {
|
||||||
buildConfigField "String", "SENTRY_DSN", "\"${property('revoltbuild.properties', 'sentry.dsn', 'RVX_SENTRY_DSN')}\""
|
buildConfigField "String", "SENTRY_DSN", "\"${property('revoltbuild.properties', 'sentry.dsn', 'RVX_SENTRY_DSN')}\""
|
||||||
|
buildConfigField "Boolean", "ANALYSIS_ENABLED", "${property('revoltbuild.properties', 'analysis.enabled', 'RVX_ANALYSIS_ENABLED')}"
|
||||||
|
buildConfigField "String", "ANALYSIS_BASEURL", "\"${property('revoltbuild.properties', 'analysis.base_url', 'RVX_ANALYSIS_BASEURL')}\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|
@ -112,9 +116,9 @@ dependencies {
|
||||||
implementation 'androidx.compose.material:material'
|
implementation 'androidx.compose.material:material'
|
||||||
implementation 'androidx.compose.material3:material3'
|
implementation 'androidx.compose.material3:material3'
|
||||||
implementation "androidx.compose.ui:ui-tooling-preview"
|
implementation "androidx.compose.ui:ui-tooling-preview"
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.0'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'
|
||||||
implementation 'androidx.activity:activity-compose:1.6.1'
|
implementation 'androidx.activity:activity-compose:1.7.0'
|
||||||
|
|
||||||
// Accompanist - Jetpack Compose Extensions
|
// Accompanist - Jetpack Compose Extensions
|
||||||
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
|
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,14 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.navigation.compose.dialog
|
||||||
import chat.revolt.api.settings.GlobalState
|
import chat.revolt.api.settings.GlobalState
|
||||||
import chat.revolt.screens.SplashScreen
|
import chat.revolt.screens.SplashScreen
|
||||||
import chat.revolt.screens.about.AboutScreen
|
import chat.revolt.screens.about.AboutScreen
|
||||||
import chat.revolt.screens.about.AttributionScreen
|
import chat.revolt.screens.about.AttributionScreen
|
||||||
import chat.revolt.screens.about.PlaceholderScreen
|
import chat.revolt.screens.about.PlaceholderScreen
|
||||||
import chat.revolt.screens.chat.ChatRouterScreen
|
import chat.revolt.screens.chat.ChatRouterScreen
|
||||||
|
import chat.revolt.screens.chat.dialogs.FeedbackDialog
|
||||||
import chat.revolt.screens.login.GreeterScreen
|
import chat.revolt.screens.login.GreeterScreen
|
||||||
import chat.revolt.screens.login.LoginScreen
|
import chat.revolt.screens.login.LoginScreen
|
||||||
import chat.revolt.screens.login.MfaScreen
|
import chat.revolt.screens.login.MfaScreen
|
||||||
|
|
@ -111,6 +113,7 @@ fun AppEntrypoint() {
|
||||||
|
|
||||||
composable("settings") { SettingsScreen(navController) }
|
composable("settings") { SettingsScreen(navController) }
|
||||||
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
|
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
|
||||||
|
dialog("settings/feedback") { FeedbackDialog(navController) }
|
||||||
|
|
||||||
composable("about") { AboutScreen(navController) }
|
composable("about") { AboutScreen(navController) }
|
||||||
composable("about/oss") { AttributionScreen(navController) }
|
composable("about/oss") { AttributionScreen(navController) }
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,27 @@ import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import chat.revolt.BuildConfig
|
||||||
import chat.revolt.api.realtime.DisconnectionState
|
import chat.revolt.api.realtime.DisconnectionState
|
||||||
import chat.revolt.api.realtime.RealtimeSocket
|
import chat.revolt.api.realtime.RealtimeSocket
|
||||||
import chat.revolt.api.routes.user.fetchSelf
|
import chat.revolt.api.routes.user.fetchSelf
|
||||||
import chat.revolt.api.schemas.*
|
import chat.revolt.api.schemas.Channel
|
||||||
|
import chat.revolt.api.schemas.Emoji
|
||||||
|
import chat.revolt.api.schemas.Message
|
||||||
|
import chat.revolt.api.schemas.Server
|
||||||
|
import chat.revolt.api.schemas.User
|
||||||
import chat.revolt.api.unreads.Unreads
|
import chat.revolt.api.unreads.Unreads
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.engine.okhttp.*
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.DefaultRequest
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.HttpRequestRetry
|
||||||
import io.ktor.client.plugins.logging.*
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.client.plugins.websocket.*
|
import io.ktor.client.plugins.defaultRequest
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.client.plugins.logging.LogLevel
|
||||||
|
import io.ktor.client.plugins.logging.Logging
|
||||||
|
import io.ktor.client.plugins.websocket.WebSockets
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
@ -61,6 +70,10 @@ val RevoltHttp = HttpClient(OkHttp) {
|
||||||
|
|
||||||
defaultRequest {
|
defaultRequest {
|
||||||
url(REVOLT_BASE)
|
url(REVOLT_BASE)
|
||||||
|
header(
|
||||||
|
"User-Agent",
|
||||||
|
"Ktor RevoltAndroid/${BuildConfig.VERSION_NAME} (Android ${android.os.Build.VERSION.SDK_INT}; ${android.os.Build.MANUFACTURER} ${android.os.Build.DEVICE})"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience wrapper around [TextField] that sets the [KeyboardOptions] and [VisualTransformation]
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FormTextField(
|
fun FormTextField(
|
||||||
|
|
@ -18,14 +21,19 @@ fun FormTextField(
|
||||||
onChange: (it: String) -> Unit,
|
onChange: (it: String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
type: KeyboardType = KeyboardType.Text,
|
type: KeyboardType = KeyboardType.Text,
|
||||||
|
supportingText: @Composable (() -> Unit)? = null,
|
||||||
|
singleLine: Boolean = true,
|
||||||
|
enabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
TextField(
|
TextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onChange,
|
onValueChange = onChange,
|
||||||
singleLine = true,
|
singleLine = singleLine,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = type),
|
keyboardOptions = KeyboardOptions(keyboardType = type),
|
||||||
visualTransformation = if (type == KeyboardType.Password) PasswordVisualTransformation() else VisualTransformation.None,
|
visualTransformation = if (type == KeyboardType.Password) PasswordVisualTransformation() else VisualTransformation.None,
|
||||||
label = { Text(label) },
|
label = { Text(label) },
|
||||||
|
supportingText = supportingText,
|
||||||
|
enabled = enabled,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,268 @@
|
||||||
|
package chat.revolt.screens.chat.dialogs
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconToggleButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.BuildConfig
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.api.REVOLT_BASE
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.RevoltHttp
|
||||||
|
import chat.revolt.components.generic.FormTextField
|
||||||
|
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.coroutines.launch
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
enum class FeedbackType(val value: String) {
|
||||||
|
Satisfaction("satisfaction"),
|
||||||
|
FeatureRequest("featurerequest"),
|
||||||
|
BugReport("bug"),
|
||||||
|
UxFeedback("ux"),
|
||||||
|
Performance("performance"),
|
||||||
|
Security("security"),
|
||||||
|
Other("other")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FeedbackBody(
|
||||||
|
val type: String,
|
||||||
|
val message: String,
|
||||||
|
val api_host: String,
|
||||||
|
val app_id: String,
|
||||||
|
val app_version: String,
|
||||||
|
val app_build: String,
|
||||||
|
val android_api: String,
|
||||||
|
val android_device: String,
|
||||||
|
val android_manufacturer: String,
|
||||||
|
@SerialName("id_for_spam_protection_pls_dont_spam_but_if_you_do_i_will_know")
|
||||||
|
val author: String
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun sendFeedback(type: FeedbackType, message: String): String {
|
||||||
|
val response = RevoltHttp.post("${BuildConfig.ANALYSIS_BASEURL}/api/feedback/android") {
|
||||||
|
setBody(
|
||||||
|
FeedbackBody(
|
||||||
|
type = type.value,
|
||||||
|
message = message,
|
||||||
|
api_host = REVOLT_BASE,
|
||||||
|
app_id = BuildConfig.APPLICATION_ID,
|
||||||
|
app_version = BuildConfig.VERSION_NAME,
|
||||||
|
app_build = BuildConfig.VERSION_CODE.toString(),
|
||||||
|
android_api = android.os.Build.VERSION.SDK_INT.toString(),
|
||||||
|
android_device = android.os.Build.DEVICE,
|
||||||
|
android_manufacturer = android.os.Build.MANUFACTURER,
|
||||||
|
author = RevoltAPI.selfId ?: "RevoltAPI.selfId is null"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("FeedbackDialog", "Feedback sent: ${response.bodyAsText()}")
|
||||||
|
|
||||||
|
return response.bodyAsText()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FeedbackDialog(navController: NavController) {
|
||||||
|
if (!BuildConfig.ANALYSIS_ENABLED) {
|
||||||
|
AlertDialog(onDismissRequest = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}, title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.settings_feedback_disabled_title),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}, text = {
|
||||||
|
Text(text = stringResource(id = R.string.settings_feedback_disabled_message))
|
||||||
|
}, confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(id = R.string.ok))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val category = mapOf(
|
||||||
|
FeedbackType.Satisfaction to stringResource(R.string.settings_feedback_category_satisfaction),
|
||||||
|
FeedbackType.FeatureRequest to stringResource(R.string.settings_feedback_category_feature),
|
||||||
|
FeedbackType.BugReport to stringResource(R.string.settings_feedback_category_bug),
|
||||||
|
FeedbackType.UxFeedback to stringResource(R.string.settings_feedback_category_ux),
|
||||||
|
FeedbackType.Performance to stringResource(R.string.settings_feedback_category_performance),
|
||||||
|
FeedbackType.Security to stringResource(R.string.settings_feedback_category_security),
|
||||||
|
FeedbackType.Other to stringResource(R.string.settings_feedback_category_other)
|
||||||
|
)
|
||||||
|
val categoryDropdownExpanded = remember { mutableStateOf(false) }
|
||||||
|
val categoryDropdownSelected = remember { mutableStateOf(FeedbackType.Satisfaction) }
|
||||||
|
val message = remember { mutableStateOf("") }
|
||||||
|
val error = remember { mutableStateOf("") }
|
||||||
|
val sending = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.settings_feedback),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.settings_feedback_introduction)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Box {
|
||||||
|
TextField(
|
||||||
|
value = category[categoryDropdownSelected.value]
|
||||||
|
?: stringResource(id = R.string.unknown),
|
||||||
|
onValueChange = {},
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.settings_feedback_category),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
IconToggleButton(
|
||||||
|
checked = categoryDropdownExpanded.value,
|
||||||
|
onCheckedChange = {
|
||||||
|
categoryDropdownExpanded.value = it
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ArrowDropDown,
|
||||||
|
contentDescription = stringResource(id = R.string.settings_feedback_category)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = categoryDropdownExpanded.value,
|
||||||
|
onDismissRequest = {
|
||||||
|
categoryDropdownExpanded.value = false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
category.forEach { (key, value) ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(text = value)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
categoryDropdownSelected.value = key
|
||||||
|
categoryDropdownExpanded.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
value = message.value,
|
||||||
|
label = stringResource(id = R.string.settings_feedback_message),
|
||||||
|
onChange = {
|
||||||
|
message.value = it
|
||||||
|
},
|
||||||
|
supportingText = {
|
||||||
|
Text(
|
||||||
|
text = "${message.value.length}/1250",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
enabled = !sending.value,
|
||||||
|
singleLine = false,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
modifier = Modifier.testTag("feedback_cancel"),
|
||||||
|
enabled = !sending.value,
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
if (message.value.length > 1250) {
|
||||||
|
error.value =
|
||||||
|
context.getString(R.string.settings_feedback_message_too_long, 1250)
|
||||||
|
} else {
|
||||||
|
error.value = ""
|
||||||
|
sending.value = true
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val result =
|
||||||
|
sendFeedback(categoryDropdownSelected.value, message.value)
|
||||||
|
Log.d("FeedbackDialog", "Feedback sent with result: $result")
|
||||||
|
|
||||||
|
if (result.isBlank()) {
|
||||||
|
navController.popBackStack()
|
||||||
|
} else {
|
||||||
|
error.value = result
|
||||||
|
Toast.makeText(context, error.value, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("FeedbackDialog", "Error sending feedback", e)
|
||||||
|
error.value = context.getString(R.string.settings_feedback_error)
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !sending.value && message.value.isNotBlank(),
|
||||||
|
modifier = Modifier.testTag("feedback_submit")
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.report_submit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ import chat.revolt.components.chat.Message
|
||||||
import chat.revolt.components.generic.FormTextField
|
import chat.revolt.components.generic.FormTextField
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
enum class ReportingState {
|
enum class ReportFlowState {
|
||||||
Reason,
|
Reason,
|
||||||
Sending,
|
Sending,
|
||||||
Done,
|
Done,
|
||||||
|
|
@ -52,13 +52,13 @@ fun ReportMessageDialog(
|
||||||
val author = RevoltAPI.userCache[message.author]
|
val author = RevoltAPI.userCache[message.author]
|
||||||
val messageIsBridged = author?.let { author.bot != null && message.masquerade != null } ?: false
|
val messageIsBridged = author?.let { author.bot != null && message.masquerade != null } ?: false
|
||||||
|
|
||||||
val state = remember { mutableStateOf(ReportingState.Reason) }
|
val state = remember { mutableStateOf(ReportFlowState.Reason) }
|
||||||
|
|
||||||
val selectedReason = remember { mutableStateOf("Illegal") }
|
val selectedReason = remember { mutableStateOf("Illegal") }
|
||||||
val userAddedContext = remember { mutableStateOf("") }
|
val userAddedContext = remember { mutableStateOf("") }
|
||||||
|
|
||||||
when (state.value) {
|
when (state.value) {
|
||||||
ReportingState.Reason -> {
|
ReportFlowState.Reason -> {
|
||||||
val reasons = mapOf(
|
val reasons = mapOf(
|
||||||
"Illegal" to stringResource(id = R.string.report_reason_content_illegal),
|
"Illegal" to stringResource(id = R.string.report_reason_content_illegal),
|
||||||
"PromotesHarm" to stringResource(id = R.string.report_reason_content_promotes_harm),
|
"PromotesHarm" to stringResource(id = R.string.report_reason_content_promotes_harm),
|
||||||
|
|
@ -169,19 +169,18 @@ fun ReportMessageDialog(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.report_reason_additional_hint),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
FormTextField(
|
FormTextField(
|
||||||
value = userAddedContext.value,
|
value = userAddedContext.value,
|
||||||
label = stringResource(id = R.string.report_reason_additional),
|
label = stringResource(id = R.string.report_reason_additional),
|
||||||
onChange = {
|
onChange = {
|
||||||
userAddedContext.value = it
|
userAddedContext.value = it
|
||||||
})
|
},
|
||||||
|
supportingText = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.report_reason_additional_hint)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
|
@ -197,7 +196,7 @@ fun ReportMessageDialog(
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
state.value = ReportingState.Sending
|
state.value = ReportFlowState.Sending
|
||||||
},
|
},
|
||||||
modifier = Modifier.testTag("report_send")
|
modifier = Modifier.testTag("report_send")
|
||||||
) {
|
) {
|
||||||
|
|
@ -207,7 +206,7 @@ fun ReportMessageDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportingState.Sending -> {
|
ReportFlowState.Sending -> {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = {},
|
onDismissRequest = {},
|
||||||
title = {
|
title = {
|
||||||
|
|
@ -231,9 +230,9 @@ fun ReportMessageDialog(
|
||||||
ContentReportReason.valueOf(selectedReason.value),
|
ContentReportReason.valueOf(selectedReason.value),
|
||||||
userAddedContext.value
|
userAddedContext.value
|
||||||
)
|
)
|
||||||
state.value = ReportingState.Done
|
state.value = ReportFlowState.Done
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
state.value = ReportingState.Error
|
state.value = ReportFlowState.Error
|
||||||
Log.e("ReportMessageDialog", "Failed to report message", e)
|
Log.e("ReportMessageDialog", "Failed to report message", e)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +245,7 @@ fun ReportMessageDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportingState.Done -> {
|
ReportFlowState.Done -> {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
|
|
@ -315,7 +314,7 @@ fun ReportMessageDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportingState.Error -> {
|
ReportFlowState.Error -> {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
package chat.revolt.screens.settings
|
package chat.revolt.screens.settings
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Build
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -73,6 +77,40 @@ fun SettingsScreen(
|
||||||
) {
|
) {
|
||||||
navController.navigate("about")
|
navController.navigate("about")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
SheetClickable(
|
||||||
|
icon = { modifier ->
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Build,
|
||||||
|
contentDescription = stringResource(id = R.string.settings_feedback),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { textStyle ->
|
||||||
|
Text(text = stringResource(id = R.string.settings_feedback), style = textStyle)
|
||||||
|
},
|
||||||
|
modifier = Modifier.testTag("settings_view_feedback")
|
||||||
|
) {
|
||||||
|
navController.navigate("settings/feedback")
|
||||||
|
}
|
||||||
|
|
||||||
|
SheetClickable(
|
||||||
|
icon = { modifier ->
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(id = R.string.logout),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { textStyle ->
|
||||||
|
Text(text = stringResource(id = R.string.logout), style = textStyle)
|
||||||
|
},
|
||||||
|
modifier = Modifier.testTag("settings_view_logout")
|
||||||
|
) {
|
||||||
|
Toast.makeText(navController.context, "Not implemented yet", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,4 +211,21 @@
|
||||||
<string name="settings_appearance_theme_m3dynamic">Material You</string>
|
<string name="settings_appearance_theme_m3dynamic">Material You</string>
|
||||||
<string name="settings_appearance_theme_m3dynamic_unsupported">Material You (unsupported)</string>
|
<string name="settings_appearance_theme_m3dynamic_unsupported">Material You (unsupported)</string>
|
||||||
<string name="settings_appearance_theme_m3dynamic_unsupported_toast">Material You is not supported on this device.</string>
|
<string name="settings_appearance_theme_m3dynamic_unsupported_toast">Material You is not supported on this device.</string>
|
||||||
|
|
||||||
|
<string name="settings_feedback">Feedback</string>
|
||||||
|
<string name="settings_feedback_introduction">Any feedback you have for Revolt is greatly appreciated and all feedback is read by the development team of our Android app.</string>
|
||||||
|
<string name="settings_feedback_category">Category</string>
|
||||||
|
<string name="settings_feedback_category_satisfaction">General-purpose feedback</string>
|
||||||
|
<string name="settings_feedback_category_feature">Feature request</string>
|
||||||
|
<string name="settings_feedback_category_bug">Bug report</string>
|
||||||
|
<string name="settings_feedback_category_ux">User experience (UI/UX)</string>
|
||||||
|
<string name="settings_feedback_category_performance">Performance</string>
|
||||||
|
<string name="settings_feedback_category_security">Security</string>
|
||||||
|
<string name="settings_feedback_category_other">Other</string>
|
||||||
|
<string name="settings_feedback_message">Message</string>
|
||||||
|
<string name="settings_feedback_message_too_long">Message is too long. Please shorten it to %1$d characters or less.</string>
|
||||||
|
<string name="settings_feedback_error">An error occurred while sending your feedback. Please try again later.</string>
|
||||||
|
|
||||||
|
<string name="settings_feedback_disabled_title">Feedback unavailable</string>
|
||||||
|
<string name="settings_feedback_disabled_message">Feedback is not available on this build of Revolt. Support for self-built versions is limited.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
sentry.dsn=
|
sentry.dsn=
|
||||||
|
analysis.enabled=false
|
||||||
|
analysis.base_url=
|
||||||
Loading…
Reference in New Issue