Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions app/src/main/java/chat/stoat/activities/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ import chat.stoat.BuildConfig
import chat.stoat.R
import chat.stoat.StoatApplication
import chat.stoat.api.HitRateLimitException
import chat.stoat.api.STOAT_BASE
import chat.stoat.api.STOAT_BASE_DEFAULT
import chat.stoat.api.STOAT_WEB_APP_DEFAULT
import chat.stoat.api.StoatAPI
import chat.stoat.api.StoatHttp
import chat.stoat.api.api
Expand All @@ -86,6 +89,7 @@ import chat.stoat.api.settings.Experiments
import chat.stoat.api.settings.GeoStateProvider
import chat.stoat.api.settings.LoadedSettings
import chat.stoat.api.settings.SyncedSettings
import chat.stoat.api.updateStoatWebApp
import chat.stoat.composables.generic.HealthAlert
import chat.stoat.composables.voice.VoicePermissionSwitch
import chat.stoat.core.model.schemas.HealthNotice
Expand Down Expand Up @@ -207,6 +211,11 @@ class MainActivityViewModel @Inject constructor(

isConnected.emit(hasInternetConnection())

val stoatInstanceUrl = kvStorage.get("stoatInstanceUrl");
if(!stoatInstanceUrl.isNullOrBlank()) {
updateStoatWebApp(stoatInstanceUrl);
}

Log.d("MainActivity", "Checking if we can reach Stoat")

if (!isConnected.value) return@launch startWithoutDestination()
Expand All @@ -222,13 +231,29 @@ class MainActivityViewModel @Inject constructor(
"We have a session token, checking if it's valid and if we can still reach Stoat"
)

val canReachStoat = canReachStoat()
val valid = try {
StoatAPI.checkSessionToken(token)
} catch (e: Throwable) {
false
var valid = false;
var canReachStoat = false;
for(attempt in 1..2) {
canReachStoat = canReachStoat();
if(canReachStoat) {
valid = try {
StoatAPI.checkSessionToken(token)
} catch (e: Throwable) {
false
}
} else {
Log.d("MainActivity", "Cannot reach $STOAT_BASE, trying $STOAT_BASE_DEFAULT");
// Fall back to the default instance,
// otherwise the user might get stuck trying to connect to an unreachable instance
updateStoatWebApp(STOAT_WEB_APP_DEFAULT);
}

if(canReachStoat && valid) {
break;
}
}


if (canReachStoat && !valid) {
Log.d("MainActivity", "Session token is invalid, could not log in")
couldNotLogIn.emit(true)
Expand Down
57 changes: 53 additions & 4 deletions app/src/main/java/chat/stoat/api/StoatAPI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.compose.runtime.mutableStateMapOf
import androidx.core.net.toUri
import chat.stoat.BuildConfig
import chat.stoat.StoatApplication
import chat.stoat.api.StoatAPI.initialize
Expand Down Expand Up @@ -56,20 +57,68 @@ import chat.stoat.core.model.schemas.Channel as ChannelSchema

private const val USE_ALPHA_API = false

val STOAT_BASE =
val STOAT_BASE_DEFAULT =
if (USE_ALPHA_API) "https://alpha.revolt.chat/api" else "https://api.stoat.chat/0.8"

var STOAT_BASE = STOAT_BASE_DEFAULT

const val STOAT_SUPPORT = "https://support.stoat.chat"
const val STOAT_MARKETING = "https://stoat.chat"
val STOAT_FILES =
val STOAT_FILES_DEFAULT =
if (USE_ALPHA_API) "https://alpha.revolt.chat/autumn" else "https://cdn.stoatusercontent.com"
var STOAT_FILES = STOAT_FILES_DEFAULT;
val STOAT_PROXY =
if (USE_ALPHA_API) "https://alpha.revolt.chat/january" else "https://proxy.stoatusercontent.com"
const val STOAT_WEB_APP = "https://stoat.chat"
const val STOAT_WEB_APP_DEFAULT = "https://stoat.chat"
var STOAT_WEB_APP = "https://stoat.chat"



const val STOAT_INVITES = "https://stt.gg"
val STOAT_WEBSOCKET =
val STOAT_WEBSOCKET_DEFAULT =
if (USE_ALPHA_API) "wss://alpha.revolt.chat/ws" else "wss://events.stoat.chat"
var STOAT_WEBSOCKET = STOAT_WEBSOCKET_DEFAULT
const val STOAT_KJBOOK = "https://stoatchat.github.io/for-android"

fun updateStoatWebApp(webApp: String = STOAT_WEB_APP_DEFAULT) {
var sanitisedBaseUrl = webApp.trim();

if(sanitisedBaseUrl.isBlank()) {
sanitisedBaseUrl = STOAT_WEB_APP_DEFAULT;
}

if(!sanitisedBaseUrl.matches(Regex("^https?://.+"))) {
// Default to https if no scheme provided
sanitisedBaseUrl = "https://${sanitisedBaseUrl}";
}

var parsed = sanitisedBaseUrl.toUri();

val portPart = if (parsed.port == 80 || parsed.port == 443) "" else ":${parsed.port}";
val root = "${parsed.scheme}://${parsed.host}${portPart}";
val rootWithPort = "${parsed.scheme}://${parsed.host}:${parsed.port}";
if(sanitisedBaseUrl.matches(Regex("^$root/+$")) || sanitisedBaseUrl.matches(Regex("^$rootWithPort/+$"))) {
// Remove trailing slashes
// (not sure if it'd actually cause an issue but might as well clean it up)
sanitisedBaseUrl = rootWithPort;
}

val isDefaultInstance = sanitisedBaseUrl == STOAT_WEB_APP_DEFAULT;

if(!isDefaultInstance) {
STOAT_WEB_APP = sanitisedBaseUrl;
STOAT_BASE = "$sanitisedBaseUrl/api";
val wssRoot = sanitisedBaseUrl.replace(Regex("^http(s?):"), "ws$1:");
STOAT_WEBSOCKET = "$wssRoot/ws";
STOAT_FILES = "$sanitisedBaseUrl/autumn";
} else {
STOAT_WEB_APP = STOAT_WEB_APP_DEFAULT;
STOAT_BASE = STOAT_BASE_DEFAULT;
STOAT_WEBSOCKET = STOAT_WEBSOCKET_DEFAULT;
STOAT_FILES = STOAT_FILES_DEFAULT;
}
}

fun String.api(): String {
return "$STOAT_BASE$this"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ fun FormTextField(
action: ImeAction = ImeAction.Done,
supportingText: @Composable (() -> Unit)? = null,
singleLine: Boolean = true,
enabled: Boolean = true
enabled: Boolean = true,
placeholder: @Composable (() -> Unit)? = null,
) {
TextField(
value = value,
Expand All @@ -34,6 +35,7 @@ fun FormTextField(
label = { Text(label) },
supportingText = supportingText,
enabled = enabled,
modifier = modifier
modifier = modifier,
placeholder = placeholder
)
}
57 changes: 51 additions & 6 deletions app/src/main/java/chat/stoat/screens/login/LoginScreen.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package chat.stoat.screens.login

import android.content.res.Configuration
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
Expand All @@ -12,8 +13,12 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.input.TextObfuscationMode
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand All @@ -31,6 +36,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
Expand All @@ -50,10 +56,13 @@ import androidx.navigation.NavController
import chat.stoat.R
import chat.stoat.StoatApplication
import chat.stoat.api.STOAT_WEB_APP
import chat.stoat.api.STOAT_WEB_APP_DEFAULT
import chat.stoat.api.StoatAPI
import chat.stoat.api.routes.account.EmailPasswordAssessment
import chat.stoat.api.routes.account.negotiateAuthentication
import chat.stoat.api.routes.onboard.needsOnboarding
import chat.stoat.api.updateStoatWebApp
import chat.stoat.composables.generic.CollapsibleCard
import chat.stoat.composables.generic.FormTextField
import chat.stoat.composables.generic.Weblink
import chat.stoat.persistence.KVStorage
Expand All @@ -74,6 +83,10 @@ class LoginViewModel @Inject constructor(
val password: String
get() = _password

private var _stoatInstanceUrl by mutableStateOf(STOAT_WEB_APP)
val stoatInstanceUrl: String
get() = _stoatInstanceUrl

private var _error by mutableStateOf<String?>(null)
val error: String?
get() = _error
Expand Down Expand Up @@ -120,14 +133,15 @@ class LoginViewModel @Inject constructor(
kvStorage.set("sessionId", id)

val onboard = needsOnboarding(token)
if (onboard) {
_navigateTo = "onboarding"
if (onboard) { _navigateTo = "onboarding"
return@launch
}

StoatAPI.loginAs(token)
StoatAPI.setSessionId(response.firstUserHints.token)

kvStorage.set("stoatInstanceUrl", _stoatInstanceUrl);

_navigateTo = "home"
} catch (e: Error) {
_error = e.message ?: "Unknown error"
Expand All @@ -148,6 +162,11 @@ class LoginViewModel @Inject constructor(
fun setPassword(password: String) {
_password = password
}

fun setStoatInstanceUrl(url: String = STOAT_WEB_APP) {
_stoatInstanceUrl = url;
updateStoatWebApp(url);
}
}

@Composable
Expand Down Expand Up @@ -189,12 +208,16 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi
}
}

val configuration = LocalConfiguration.current;

Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
.padding(10.dp)
.imePadding()
.safeDrawingPadding(),
.safeDrawingPadding()
.verticalScroll(rememberScrollState())
.overscroll(rememberOverscrollEffect()),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Expand All @@ -217,11 +240,16 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi
)

Column(
modifier = Modifier
.width(270.dp),
modifier = (when(configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> Modifier.fillMaxWidth()
else -> Modifier.width(270.dp)
})
.overscroll(rememberOverscrollEffect())
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {

FormTextField(
value = viewModel.email,
label = stringResource(R.string.email),
Expand Down Expand Up @@ -277,6 +305,23 @@ fun LoginScreen(navController: NavController, viewModel: LoginViewModel = hiltVi
modifier = Modifier.padding(vertical = 7.dp)
)

CollapsibleCard(
header = {Text(text = stringResource(R.string.login_advanced_options))},
modifier = Modifier.width(300.dp)
) {
FormTextField(
value = viewModel.stoatInstanceUrl,
label = stringResource(R.string.stoat_instance), // TODO: needs translations
type = KeyboardType.Uri,
action = ImeAction.Next,
onChange = viewModel::setStoatInstanceUrl,
modifier = Modifier
.padding(vertical = 5.dp),
placeholder = {Text(text = STOAT_WEB_APP_DEFAULT)},
supportingText = {Text(text = stringResource(R.string.stoat_instance_hint))}
)
}

if (viewModel.error != null) {
Text(
text = viewModel.error!!,
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
<string name="login_onboarding_heading">Find your community, connect with the world.</string>
<string name="login_onboarding_body">Stoat is one of the best ways to stay connected with your friends and community, anywhere, anytime.</string>

<string name="stoat_instance">Stoat host</string>
<string name="stoat_instance_hint">This is the base URL of the Stoat instance you\'re logging in to. If you\'re unsure, leave this field blank to use the official Stoat instance.</string>
<string name="login_advanced_options">Advanced options</string>
<string name="email">Email</string>
<string name="password">Password</string>

Expand Down
Loading