diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 49a14191..98dbc3d9 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -8,7 +8,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index fffaa64a..6e263068 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -12,7 +12,7 @@ - + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 189201d8..71369618 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,9 +30,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/activities/InviteActivity.kt b/app/src/main/java/chat/revolt/activities/InviteActivity.kt new file mode 100644 index 00000000..937035ba --- /dev/null +++ b/app/src/main/java/chat/revolt/activities/InviteActivity.kt @@ -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?>(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 = 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 = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt index 8118a91b..e9106b85 100644 --- a/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt +++ b/app/src/main/java/chat/revolt/api/realtime/RealtimeSocket.kt @@ -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 } diff --git a/app/src/main/java/chat/revolt/api/realtime/frames/receivable/ReceivableFrames.kt b/app/src/main/java/chat/revolt/api/realtime/frames/receivable/ReceivableFrames.kt index 40f0f85c..5ba65b1d 100644 --- a/app/src/main/java/chat/revolt/api/realtime/frames/receivable/ReceivableFrames.kt +++ b/app/src/main/java/chat/revolt/api/realtime/frames/receivable/ReceivableFrames.kt @@ -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, ) @Serializable diff --git a/app/src/main/java/chat/revolt/api/routes/invites/Invites.kt b/app/src/main/java/chat/revolt/api/routes/invites/Invites.kt new file mode 100644 index 00000000..51dc2602 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/routes/invites/Invites.kt @@ -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 { + 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 { + 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) +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Invites.kt b/app/src/main/java/chat/revolt/api/schemas/Invites.kt new file mode 100644 index 00000000..6f9fdadf --- /dev/null +++ b/app/src/main/java/chat/revolt/api/schemas/Invites.kt @@ -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? = null, + val server: Server? = null, +) \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Util.kt b/app/src/main/java/chat/revolt/api/schemas/Util.kt new file mode 100644 index 00000000..b50ce2c7 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/schemas/Util.kt @@ -0,0 +1,52 @@ +package chat.revolt.api.schemas + +// Result class similar to Rust std::result::Result +data class RsResult(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 ok(value: V): RsResult { + return RsResult(value, null) + } + + fun err(error: E): RsResult { + return RsResult(null, error) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/components/generic/IconPlaceholder.kt b/app/src/main/java/chat/revolt/components/generic/IconPlaceholder.kt new file mode 100644 index 00000000..c11f4d74 --- /dev/null +++ b/app/src/main/java/chat/revolt/components/generic/IconPlaceholder.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerServer.kt b/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerServer.kt index 7f06187a..fba23bcb 100644 --- a/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerServer.kt +++ b/app/src/main/java/chat/revolt/components/screens/chat/drawer/server/DrawerServer.kt @@ -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 diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt index 96520982..746adc9c 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -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") diff --git a/app/src/main/java/chat/revolt/screens/chat/sheets/AddServerSheet.kt b/app/src/main/java/chat/revolt/screens/chat/sheets/AddServerSheet.kt new file mode 100644 index 00000000..c4ee29e8 --- /dev/null +++ b/app/src/main/java/chat/revolt/screens/chat/sheets/AddServerSheet.kt @@ -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)) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt index 1818e470..ebde09b2 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/HomeScreen.kt @@ -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)) } } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt index ec2aa63f..d84fbb71 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt @@ -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 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b07a970a..815a08f9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,7 +64,7 @@ The feature you are trying to access is not ready yet, but we are steadily working on polishing it to perfection.. Sorry, this feature is not ready yet. - + %1$s is typing… %1$s are typing… Several people are typing @@ -162,6 +162,16 @@ Copied server ID to clipboard Mark as read + Add a server + Join by invite code or link + Invite code or link + Enter a link like rvlt.gg/Testers or an invite code like Testers + Invite code or link + Join + Create a new server + Create a new server + This feature is currently under construction. + Report Cancel @@ -207,6 +217,16 @@ Block Don\'t block + You\'ve been invited to join this server. Would you like to join? + Join + Cancel + You are already a member of this server. + There was an error + No invite code was specified. + Could not find an invite with the specified code. + You are banned from this server. + An unknown error occurred. + Appearance Theme System