package chat.revolt.activities import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.background 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.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.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.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.graphics.toArgb import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource 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.core.view.WindowCompat 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.LoadedSettings import chat.revolt.api.settings.SyncedSettings import chat.revolt.composables.generic.IconPlaceholder import chat.revolt.composables.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 WindowCompat.setDecorFitsSystemWindows(window, false) window.statusBarColor = Color.Transparent.toArgb() 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?>(null) val inviteResult: RsResult? get() = _inviteResult fun setInviteResult(inviteResult: RsResult?) { _inviteResult = inviteResult } private var _joinResult by mutableStateOf?>(null) val joinResult: RsResult? get() = _joinResult fun setJoinResult(joinResult: RsResult?) { _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 = LoadedSettings.theme, colourOverrides = SyncedSettings.android.colourOverrides ) { Surface( modifier = Modifier .background(MaterialTheme.colorScheme.background) .fillMaxSize() ) { if (inviteCode == null) { NoInviteSpecifiedError(onDismissRequest = onFinish) } else { if (inviteValid == null) { Column( modifier = Modifier .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { CircularProgressIndicator( modifier = Modifier .size(48.dp) ) } } 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}/${invite?.serverBanner?.filename}", 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.surfaceContainer) .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { if (invite?.serverIcon != null) { RemoteImage( url = "$REVOLT_FILES/icons/${invite.serverIcon.id}/${invite.serverIcon.filename}", allowAnimation = false, 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 .testTag("accept_invite") ) { Text(text = stringResource(id = R.string.invite_join)) } Spacer(modifier = Modifier.width(8.dp)) TextButton( onClick = onFinish, modifier = Modifier .testTag("decline_invite") ) { Text(text = stringResource(id = R.string.invite_cancel)) } } } } } } } } } @Composable fun InvalidInviteError(error: RevoltError? = null, onDismissRequest: () -> Unit) { AlertDialog( onDismissRequest = onDismissRequest, icon = { Icon( painter = painterResource(R.drawable.icn_error_24dp), 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( painter = painterResource(R.drawable.icn_error_24dp), 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 = {} ) }