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="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleHome" value="$PROJECT_DIR$/../../../../Gradle" />
|
<option name="gradleHome" value="$PROJECT_DIR$/../../../../Gradle" />
|
||||||
<option name="gradleJvm" value="JDK" />
|
<option name="gradleJvm" value="jbr-17" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</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" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,42 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.WebChallengeActivity"
|
android:name=".activities.WebChallengeActivity"
|
||||||
android:theme="@style/Theme.Revolt" />
|
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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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.MessageUpdateFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.PongFrame
|
import chat.revolt.api.realtime.frames.receivable.PongFrame
|
||||||
import chat.revolt.api.realtime.frames.receivable.ReadyFrame
|
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.receivable.UserUpdateFrame
|
||||||
import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame
|
import chat.revolt.api.realtime.frames.sendable.AuthorizationFrame
|
||||||
import chat.revolt.api.realtime.frames.sendable.PingFrame
|
import chat.revolt.api.realtime.frames.sendable.PingFrame
|
||||||
|
|
@ -281,6 +282,22 @@ object RealtimeSocket {
|
||||||
RevoltAPI.unreads.processExternalAck(channelAckFrame.id, channelAckFrame.messageId)
|
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" -> {
|
"Authenticated" -> {
|
||||||
// No effect
|
// No effect
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,8 @@ data class ChannelAckFrame(
|
||||||
data class ServerCreateFrame(
|
data class ServerCreateFrame(
|
||||||
val type: String = "ServerCreate",
|
val type: String = "ServerCreate",
|
||||||
val id: String,
|
val id: String,
|
||||||
val server: Server
|
val server: Server,
|
||||||
|
val channels: List<Channel>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@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.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.LocalContentColor
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import chat.revolt.api.REVOLT_FILES
|
import chat.revolt.api.REVOLT_FILES
|
||||||
|
import chat.revolt.components.generic.IconPlaceholder
|
||||||
import chat.revolt.components.generic.RemoteImage
|
import chat.revolt.components.generic.RemoteImage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -33,7 +29,8 @@ fun DrawerServer(
|
||||||
) {
|
) {
|
||||||
val unreadIndicatorAlpha = animateFloatAsState(
|
val unreadIndicatorAlpha = animateFloatAsState(
|
||||||
if (hasUnreads) 1f else 0f,
|
if (hasUnreads) 1f else 0f,
|
||||||
animationSpec = spring()
|
animationSpec = spring(),
|
||||||
|
label = "Unread indicator alpha"
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -50,22 +47,14 @@ fun DrawerServer(
|
||||||
description = serverName
|
description = serverName
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Box(
|
IconPlaceholder(
|
||||||
contentAlignment = Alignment.Center,
|
name = serverName,
|
||||||
|
onClick = onClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.size(48.dp)
|
.size(48.dp)
|
||||||
.clip(CircleShape)
|
.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
|
// Unread indicator
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package chat.revolt.screens.chat
|
package chat.revolt.screens.chat
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -29,7 +28,6 @@ import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
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.components.screens.chat.rememberDoubleDrawerState
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
import chat.revolt.screens.chat.dialogs.safety.ReportMessageDialog
|
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.ChannelContextSheet
|
||||||
import chat.revolt.screens.chat.sheets.ChannelInfoSheet
|
import chat.revolt.screens.chat.sheets.ChannelInfoSheet
|
||||||
import chat.revolt.screens.chat.sheets.MessageContextSheet
|
import chat.revolt.screens.chat.sheets.MessageContextSheet
|
||||||
|
|
@ -151,7 +150,6 @@ class ChatRouterViewModel @Inject constructor(
|
||||||
fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hiltViewModel()) {
|
fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hiltViewModel()) {
|
||||||
val drawerState = rememberDoubleDrawerState()
|
val drawerState = rememberDoubleDrawerState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
val bottomSheetNavigator = rememberBottomSheetNavigator()
|
val bottomSheetNavigator = rememberBottomSheetNavigator()
|
||||||
|
|
@ -246,11 +244,7 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
|
||||||
|
|
||||||
DrawerServerlikeIcon(
|
DrawerServerlikeIcon(
|
||||||
onClick = {
|
onClick = {
|
||||||
Toast.makeText(
|
navController.navigate("add_server")
|
||||||
context,
|
|
||||||
context.getString(R.string.comingsoon_toast),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -336,6 +330,9 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil
|
||||||
bottomSheet("status") {
|
bottomSheet("status") {
|
||||||
StatusSheet(navController = navController, topNav = topNav)
|
StatusSheet(navController = navController, topNav = topNav)
|
||||||
}
|
}
|
||||||
|
bottomSheet("add_server") {
|
||||||
|
AddServerSheet()
|
||||||
|
}
|
||||||
|
|
||||||
dialog("report/message/{messageId}") { backStackEntry ->
|
dialog("report/message/{messageId}") { backStackEntry ->
|
||||||
val messageId = backStackEntry.arguments?.getString("messageId")
|
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
|
package chat.revolt.screens.chat.views
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
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.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
|
@ -57,19 +49,5 @@ fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hi
|
||||||
},
|
},
|
||||||
modifier = Modifier.testTag("logout_from_home")
|
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()
|
CircularProgressIndicator()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +140,7 @@ fun ChannelScreen(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
ChannelIcon(
|
ChannelIcon(
|
||||||
channelType = channel.channelType!!,
|
channelType = channel.channelType,
|
||||||
modifier = Modifier.padding(end = 8.dp)
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -331,7 +331,7 @@ fun ChannelScreen(
|
||||||
onAddAttachment = {
|
onAddAttachment = {
|
||||||
pickFileLauncher.launch(arrayOf("*/*"))
|
pickFileLauncher.launch(arrayOf("*/*"))
|
||||||
},
|
},
|
||||||
channelType = channel.channelType!!,
|
channelType = channel.channelType,
|
||||||
channelName = channel.name ?: channel.id!!,
|
channelName = channel.name ?: channel.id!!,
|
||||||
forceSendButton = viewModel.attachments.isNotEmpty(),
|
forceSendButton = viewModel.attachments.isNotEmpty(),
|
||||||
disabled = viewModel.attachments.isNotEmpty() && viewModel.sendingMessage
|
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_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="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_one">%1$s is typing…</string>
|
||||||
<string name="typing_many">%1$s are typing…</string>
|
<string name="typing_many">%1$s are typing…</string>
|
||||||
<string name="typing_several">Several people 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_copy_id_copied">Copied server ID to clipboard</string>
|
||||||
<string name="server_context_sheet_actions_mark_read">Mark as read</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">Report</string>
|
||||||
<string name="report_cancel">Cancel</string>
|
<string name="report_cancel">Cancel</string>
|
||||||
|
|
||||||
|
|
@ -207,6 +217,16 @@
|
||||||
<string name="report_block_yes">Block</string>
|
<string name="report_block_yes">Block</string>
|
||||||
<string name="report_block_no">Don\'t 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">Appearance</string>
|
||||||
<string name="settings_appearance_theme">Theme</string>
|
<string name="settings_appearance_theme">Theme</string>
|
||||||
<string name="settings_appearance_theme_none">System</string>
|
<string name="settings_appearance_theme_none">System</string>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue