feat: invite opening, viewing, joining and handling

This commit is contained in:
Infi 2023-04-06 04:21:51 +02:00
parent f09469f7be
commit 378cbbf98c
16 changed files with 767 additions and 55 deletions

View File

@ -8,7 +8,7 @@
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="$PROJECT_DIR$/../../../../Gradle" />
<option name="gradleJvm" value="JDK" />
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

View File

@ -12,7 +12,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -30,9 +30,42 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activities.WebChallengeActivity"
android:theme="@style/Theme.Revolt" />
<activity
android:name=".activities.InviteActivity"
android:theme="@style/Theme.Revolt"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="app.revolt.chat" />
<data android:host="nightly.revolt.chat" />
<data android:pathPrefix="/invite/" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="rvlt.gg" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,350 @@
package chat.revolt.activities
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
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.fillMaxSize
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.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import chat.revolt.R
import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltError
import chat.revolt.api.routes.invites.fetchInviteByCode
import chat.revolt.api.routes.invites.joinInviteByCode
import chat.revolt.api.schemas.Invite
import chat.revolt.api.schemas.InviteJoined
import chat.revolt.api.schemas.RsResult
import chat.revolt.api.settings.GlobalState
import chat.revolt.components.generic.IconPlaceholder
import chat.revolt.components.generic.RemoteImage
import chat.revolt.ui.theme.RevoltTheme
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
import kotlinx.coroutines.launch
class InviteActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inviteCode = intent.data?.lastPathSegment
Log.d("InviteActivity", "Invite code: $inviteCode")
setContent {
InviteScreen(
inviteCode = inviteCode,
onFinish = { finish() }
)
}
}
}
class InviteViewModel : ViewModel() {
private var _loadingFinished by mutableStateOf(false)
val loadingFinished: Boolean
get() = _loadingFinished
fun setLoadingFinished(loadingFinished: Boolean) {
_loadingFinished = loadingFinished
}
private var _inviteResult by mutableStateOf<RsResult<Invite, RevoltError>?>(null)
val inviteResult: RsResult<Invite, RevoltError>?
get() = _inviteResult
fun setInviteResult(inviteResult: RsResult<Invite, RevoltError>?) {
_inviteResult = inviteResult
}
private var _joinResult by mutableStateOf<RsResult<InviteJoined, RevoltError>?>(null)
val joinResult: RsResult<InviteJoined, RevoltError>?
get() = _joinResult
fun setJoinResult(joinResult: RsResult<InviteJoined, RevoltError>?) {
_joinResult = joinResult
}
fun fetchInvite(inviteCode: String) {
viewModelScope.launch {
val result = fetchInviteByCode(inviteCode)
setInviteResult(result)
setLoadingFinished(true)
}
}
fun joinInvite(inviteCode: String) {
viewModelScope.launch {
val result = joinInviteByCode(inviteCode)
setJoinResult(result)
}
}
}
@OptIn(ExperimentalGlideComposeApi::class)
@Composable
fun InviteScreen(
inviteCode: String?,
onFinish: () -> Unit = {},
viewModel: InviteViewModel = viewModel()
) {
LaunchedEffect(inviteCode) {
if (inviteCode != null) {
viewModel.fetchInvite(inviteCode)
}
}
LaunchedEffect(viewModel.joinResult) {
if (viewModel.joinResult?.ok == true) {
onFinish()
}
}
val inviteValid = if (viewModel.loadingFinished) (viewModel.inviteResult?.ok ?: false) else null
val invite = viewModel.inviteResult?.value
RevoltTheme(requestedTheme = GlobalState.theme) {
Surface(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxSize()
) {
if (inviteCode == null) {
NoInviteSpecifiedError(onDismissRequest = onFinish)
} else {
if (inviteValid == null) {
CircularProgressIndicator()
} else if (!inviteValid || viewModel.joinResult?.err == true) {
InvalidInviteError(
error = viewModel.inviteResult?.error ?: viewModel.joinResult?.error,
onDismissRequest = onFinish
)
} else {
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
GlideImage(
model = "$REVOLT_FILES/banners/${invite?.serverBanner?.id}?max_side=256",
contentScale = ContentScale.Crop,
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
)
Box(
modifier = Modifier
.fillMaxSize()
.background(
Color.Black.copy(alpha = 0.5f)
)
)
Column(
modifier = Modifier
.padding(16.dp)
.clip(MaterialTheme.shapes.large)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (invite?.serverIcon != null) {
RemoteImage(
url = "$REVOLT_FILES/icons/${invite.serverIcon.id}?max_side=256",
description = viewModel.inviteResult?.value?.serverName
?: stringResource(id = R.string.unknown),
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
)
} else {
IconPlaceholder(
name = invite?.serverName
?: stringResource(id = R.string.unknown),
modifier = Modifier
.size(64.dp)
.clip(CircleShape),
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = viewModel.inviteResult?.value?.serverName
?: stringResource(id = R.string.unknown),
fontWeight = FontWeight.Bold,
fontSize = 24.sp,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = R.string.invite_message),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
Row {
Button(
onClick = {
viewModel.joinInvite(inviteCode)
},
modifier = Modifier
.weight(1f)
.testTag("accept_invite")
) {
Text(text = stringResource(id = R.string.invite_join))
}
Spacer(modifier = Modifier.width(8.dp))
TextButton(
onClick = onFinish,
modifier = Modifier
.weight(1f)
.testTag("decline_invite")
) {
Text(text = stringResource(id = R.string.invite_cancel))
}
}
}
}
}
}
}
}
}
@Composable
fun InvalidInviteError(
error: RevoltError? = null,
onDismissRequest: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismissRequest,
icon = {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null, // decorative
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
text = stringResource(id = R.string.invite_error_header),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
},
text = {
Column() {
Text(
text = stringResource(
id = when (error?.type) {
"NotFound" -> R.string.invite_error_invalid_invite
"Banned" -> R.string.invite_error_banned
else -> R.string.invite_error_unknown
}
),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
},
dismissButton = {
TextButton(
onClick = {
onDismissRequest()
}
) {
Text(text = stringResource(id = R.string.invite_cancel))
}
},
confirmButton = {}
)
}
@Composable
fun NoInviteSpecifiedError(onDismissRequest: () -> Unit) {
AlertDialog(
onDismissRequest = onDismissRequest,
icon = {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null, // decorative
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
text = stringResource(id = R.string.invite_error_header),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
},
text = {
Column() {
Text(
text = stringResource(id = R.string.invite_error_no_invite),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
},
dismissButton = {
TextButton(
onClick = {
onDismissRequest()
}
) {
Text(text = stringResource(id = R.string.ok))
}
},
confirmButton = {}
)
}

View File

@ -16,6 +16,7 @@ import chat.revolt.api.realtime.frames.receivable.MessageFrame
import chat.revolt.api.realtime.frames.receivable.MessageUpdateFrame
import chat.revolt.api.realtime.frames.receivable.PongFrame
import chat.revolt.api.realtime.frames.receivable.ReadyFrame
import chat.revolt.api.realtime.frames.receivable.ServerCreateFrame
import chat.revolt.api.realtime.frames.receivable.UserUpdateFrame
import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame
import chat.revolt.api.realtime.frames.sendable.PingFrame
@ -281,6 +282,22 @@ object RealtimeSocket {
RevoltAPI.unreads.processExternalAck(channelAckFrame.id, channelAckFrame.messageId)
}
"ServerCreate" -> {
val serverCreateFrame =
RevoltJson.decodeFromString(ServerCreateFrame.serializer(), rawFrame)
Log.d(
"RealtimeSocket",
"Received server create frame for ${serverCreateFrame.id}, with name ${serverCreateFrame.server.name}. Adding to cache."
)
RevoltAPI.serverCache[serverCreateFrame.id] = serverCreateFrame.server
serverCreateFrame.channels.forEach { channel ->
if (channel.id == null) return@forEach
RevoltAPI.channelCache[channel.id] = channel
}
}
"Authenticated" -> {
// No effect
}

View File

@ -151,7 +151,8 @@ data class ChannelAckFrame(
data class ServerCreateFrame(
val type: String = "ServerCreate",
val id: String,
val server: Server
val server: Server,
val channels: List<Channel>,
)
@Serializable

View File

@ -0,0 +1,47 @@
package chat.revolt.api.routes.invites
import chat.revolt.api.RevoltAPI
import chat.revolt.api.RevoltError
import chat.revolt.api.RevoltHttp
import chat.revolt.api.RevoltJson
import chat.revolt.api.schemas.Invite
import chat.revolt.api.schemas.InviteJoined
import chat.revolt.api.schemas.RsResult
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.SerializationException
suspend fun fetchInviteByCode(code: String): RsResult<Invite, RevoltError> {
val response = RevoltHttp.get("/invites/$code") {
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
}
.bodyAsText()
try {
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response)
if (error.type != "Server") return RsResult.err(error)
} catch (e: SerializationException) {
// Not an error
}
val invite = RevoltJson.decodeFromString(Invite.serializer(), response)
return RsResult.ok(invite)
}
suspend fun joinInviteByCode(code: String): RsResult<InviteJoined, RevoltError> {
val response = RevoltHttp.post("/invites/$code") {
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
}
.bodyAsText()
try {
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response)
if (error.type != "Server") return RsResult.err(error)
} catch (e: SerializationException) {
// Not an error
}
val invite = RevoltJson.decodeFromString(InviteJoined.serializer(), response)
return RsResult.ok(invite)
}

View File

@ -0,0 +1,47 @@
package chat.revolt.api.schemas
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Invite(
val type: String? = null,
val code: String? = null,
@SerialName("server_id")
val serverId: String? = null,
@SerialName("server_name")
val serverName: String? = null,
@SerialName("server_icon")
val serverIcon: AutumnResource? = null,
@SerialName("server_banner")
val serverBanner: AutumnResource? = null,
@SerialName("server_flags")
val serverFlags: Long? = null,
@SerialName("channel_id")
val channelId: String? = null,
@SerialName("channel_name")
val channelName: String? = null,
@SerialName("user_name")
val userName: String? = null,
@SerialName("user_avatar")
val userAvatar: AutumnResource? = null,
@SerialName("member_count")
val memberCount: Long? = null
)
@Serializable
data class InviteJoined(
val type: String? = null,
val channels: List<Channel>? = null,
val server: Server? = null,
)

View File

@ -0,0 +1,52 @@
package chat.revolt.api.schemas
// Result class similar to Rust std::result::Result
data class RsResult<V, E>(val value: V?, val error: E?) {
val ok: Boolean
get() = value != null
val err: Boolean
get() = error != null
fun unwrap(): V {
if (value == null) {
throw IllegalStateException("Called unwrap on RsResult with error")
}
return value
}
fun unwrapOr(default: V): V {
if (value == null) {
return default
}
return value
}
fun unwrapOrElse(default: () -> V): V {
if (value == null) {
return default()
}
return value
}
fun unwrapError(): E {
if (error == null) {
throw IllegalStateException("Called unwrapError on RsResult with value")
}
return error
}
companion object {
fun <V, E> ok(value: V): RsResult<V, E> {
return RsResult(value, null)
}
fun <V, E> err(error: E): RsResult<V, E> {
return RsResult(null, error)
}
}
}

View File

@ -0,0 +1,38 @@
package chat.revolt.components.generic
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun IconPlaceholder(
name: String,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
.then(
if (onClick != {}) Modifier.clickable(onClick = onClick)
else Modifier
)
) {
Text(
text = name.first().uppercase(),
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
}
}

View File

@ -10,18 +10,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.revolt.api.REVOLT_FILES
import chat.revolt.components.generic.IconPlaceholder
import chat.revolt.components.generic.RemoteImage
@Composable
@ -33,7 +29,8 @@ fun DrawerServer(
) {
val unreadIndicatorAlpha = animateFloatAsState(
if (hasUnreads) 1f else 0f,
animationSpec = spring()
animationSpec = spring(),
label = "Unread indicator alpha"
)
Box(
@ -50,22 +47,14 @@ fun DrawerServer(
description = serverName
)
} else {
Box(
contentAlignment = Alignment.Center,
IconPlaceholder(
name = serverName,
onClick = onClick,
modifier = Modifier
.padding(8.dp)
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp))
.clickable(onClick = onClick)
) {
Text(
text = serverName.first().uppercase(),
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
}
)
}
// Unread indicator

View File

@ -1,6 +1,5 @@
package chat.revolt.screens.chat
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
@ -29,7 +28,6 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -58,6 +56,7 @@ import chat.revolt.components.screens.chat.drawer.server.ServerDrawerSeparator
import chat.revolt.components.screens.chat.rememberDoubleDrawerState
import chat.revolt.persistence.KVStorage
import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog
import chat.revolt.screens.chat.sheets.AddServerSheet
import chat.revolt.screens.chat.sheets.ChannelContextSheet
import chat.revolt.screens.chat.sheets.ChannelInfoSheet
import chat.revolt.screens.chat.sheets.MessageContextSheet
@ -151,7 +150,6 @@ class ChatRouterViewModel @Inject constructor(
fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hiltViewModel()) {
val drawerState = rememberDoubleDrawerState()
val scope = rememberCoroutineScope()
val context = LocalContext.current
val keyboardController = LocalSoftwareKeyboardController.current
val bottomSheetNavigator = rememberBottomSheetNavigator()
@ -246,11 +244,7 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
DrawerServerlikeIcon(
onClick = {
Toast.makeText(
context,
context.getString(R.string.comingsoon_toast),
Toast.LENGTH_SHORT
).show()
navController.navigate("add_server")
}
) {
Icon(
@ -336,6 +330,9 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
bottomSheet("status") {
StatusSheet(navController = navController, topNav = topNav)
}
bottomSheet("add_server") {
AddServerSheet()
}
dialog("report/message/{messageId}") { backStackEntry ->
val messageId = backStackEntry.arguments?.getString("messageId")

View File

@ -0,0 +1,143 @@
package chat.revolt.screens.chat.sheets
import android.content.Intent
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import chat.revolt.R
import chat.revolt.activities.InviteActivity
import chat.revolt.api.REVOLT_APP
import chat.revolt.components.generic.FormTextField
import chat.revolt.components.generic.PageHeader
import chat.revolt.components.screens.home.LinkOnHome
@Composable
fun AddServerSheet() {
val context = LocalContext.current
val joinFromInviteModalOpen = remember { mutableStateOf(false) }
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
) {
if (joinFromInviteModalOpen.value) {
JoinFromInviteModal(
onDismiss = {
joinFromInviteModalOpen.value = false
}
)
}
Spacer(modifier = Modifier.height(4.dp))
PageHeader(text = stringResource(id = R.string.add_server_sheet_title))
Spacer(modifier = Modifier.height(4.dp))
LinkOnHome(
heading = stringResource(id = R.string.add_server_sheet_join_by_invite),
icon = Icons.Default.ExitToApp,
onClick = {
joinFromInviteModalOpen.value = true
}
)
Spacer(modifier = Modifier.height(4.dp))
LinkOnHome(
heading = stringResource(id = R.string.add_server_sheet_create_new),
icon = Icons.Default.Build,
onClick = {
Toast.makeText(
context,
context.getString(R.string.add_server_sheet_create_new_modal_under_construction),
Toast.LENGTH_SHORT
).show()
}
)
Spacer(modifier = Modifier.height(16.dp))
}
}
@Composable
fun JoinFromInviteModal(
onDismiss: () -> Unit,
) {
val context = LocalContext.current
val inviteCode = remember { mutableStateOf("") }
val inviteActivityResult = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
Log.d("InviteActivity", "Result: $result")
}
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(text = stringResource(id = R.string.add_server_sheet_join_by_invite_modal_title))
},
text = {
Column {
Text(text = stringResource(id = R.string.add_server_sheet_join_by_invite_modal_description))
Spacer(modifier = Modifier.height(8.dp))
FormTextField(
label = stringResource(id = R.string.add_server_sheet_join_by_invite_modal_hint),
value = inviteCode.value,
onChange = {
inviteCode.value = it
}
)
}
},
confirmButton = {
TextButton(
onClick = {
val intent = Intent(context, InviteActivity::class.java)
intent.data = if (inviteCode.value.startsWith("https://")) {
inviteCode.value.toUri()
} else {
"https://$REVOLT_APP/invite/${inviteCode.value}".toUri()
}
inviteActivityResult.launch(intent)
}
) {
Text(text = stringResource(id = R.string.add_server_sheet_join_by_invite_modal_join))
}
},
dismissButton = {
TextButton(
onClick = {
onDismiss()
}
) {
Text(text = stringResource(id = R.string.cancel))
}
}
)
}

View File

@ -1,20 +1,12 @@
package chat.revolt.screens.chat.views
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.navigation.NavController
@ -57,19 +49,5 @@ fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hi
},
modifier = Modifier.testTag("logout_from_home")
)
LinkOnHome(
heading = stringResource(id = R.string.settings),
icon = Icons.Default.Settings,
onClick = {
navController.navigate("settings")
}
)
Text(buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append("Note: ")
}
append("Settings are accessible from the top left status icon in the drawer. The link here is temporary until a tutorial is in place.")
}, modifier = Modifier.padding(16.dp))
}
}

View File

@ -117,7 +117,7 @@ fun ChannelScreen(
}
}
if (channel == null) {
if (channel?.channelType == null) {
CircularProgressIndicator()
return
}
@ -140,7 +140,7 @@ fun ChannelScreen(
verticalAlignment = Alignment.CenterVertically
) {
ChannelIcon(
channelType = channel.channelType!!,
channelType = channel.channelType,
modifier = Modifier.padding(end = 8.dp)
)
Text(
@ -331,7 +331,7 @@ fun ChannelScreen(
onAddAttachment = {
pickFileLauncher.launch(arrayOf("*/*"))
},
channelType = channel.channelType!!,
channelType = channel.channelType,
channelName = channel.name ?: channel.id!!,
forceSendButton = viewModel.attachments.isNotEmpty(),
disabled = viewModel.attachments.isNotEmpty() && viewModel.sendingMessage

View File

@ -64,7 +64,7 @@
<string name="comingsoon_body">The feature you are trying to access is not ready yet, but we are steadily working on polishing it to perfection..</string>
<string name="comingsoon_toast">Sorry, this feature is not ready yet.</string>
<string name="typing_blank"><!-- this is a hack to prevent the typing indicator from showing typing_several when it's animating away --></string>
<string name="typing_blank" translatable="false"><!-- this is a hack to prevent the typing indicator from showing typing_several when it's animating away --></string>
<string name="typing_one">%1$s is typing…</string>
<string name="typing_many">%1$s are typing…</string>
<string name="typing_several">Several people are typing</string>
@ -162,6 +162,16 @@
<string name="server_context_sheet_actions_copy_id_copied">Copied server ID to clipboard</string>
<string name="server_context_sheet_actions_mark_read">Mark as read</string>
<string name="add_server_sheet_title">Add a server</string>
<string name="add_server_sheet_join_by_invite">Join by invite code or link</string>
<string name="add_server_sheet_join_by_invite_modal_title">Invite code or link</string>
<string name="add_server_sheet_join_by_invite_modal_description">Enter a link like rvlt.gg/Testers or an invite code like Testers</string>
<string name="add_server_sheet_join_by_invite_modal_hint">Invite code or link</string>
<string name="add_server_sheet_join_by_invite_modal_join">Join</string>
<string name="add_server_sheet_create_new">Create a new server</string>
<string name="add_server_sheet_create_new_modal_title">Create a new server</string>
<string name="add_server_sheet_create_new_modal_under_construction">This feature is currently under construction.</string>
<string name="report">Report</string>
<string name="report_cancel">Cancel</string>
@ -207,6 +217,16 @@
<string name="report_block_yes">Block</string>
<string name="report_block_no">Don\'t block</string>
<string name="invite_message">You\'ve been invited to join this server. Would you like to join?</string>
<string name="invite_join">Join</string>
<string name="invite_cancel">Cancel</string>
<string name="invite_already_member">You are already a member of this server.</string>
<string name="invite_error_header">There was an error</string>
<string name="invite_error_no_invite">No invite code was specified.</string>
<string name="invite_error_invalid_invite">Could not find an invite with the specified code.</string>
<string name="invite_error_banned">You are banned from this server.</string>
<string name="invite_error_unknown">An unknown error occurred.</string>
<string name="settings_appearance">Appearance</string>
<string name="settings_appearance_theme">Theme</string>
<string name="settings_appearance_theme_none">System</string>