feat: invite opening, viewing, joining and handling
This commit is contained in:
parent
f09469f7be
commit
378cbbf98c
|
|
@ -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$" />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue