profile: Fetch the user profile on every app start

The login flow stored displayName/avatar inside one swallow-all try that
serverConfig() also had to survive first, and /users/me carries both in
its nested account object on current PeerTube versions (verified live
against 8.3.1) — so stored sessions kept showing the raw username and no
avatar. The profile now refreshes after login and whenever an existing
session starts, and the relative avatar fallback no longer points into
/api/v1.
This commit is contained in:
Jeena 2026-10-13 00:05:10 +00:00
parent a71de07969
commit 15a481b7c0
3 changed files with 63 additions and 35 deletions

View file

@ -69,45 +69,47 @@ class PeerTubeRepository(
expiresAtMillis = System.currentTimeMillis() +
(token.expiresIn ?: 86400L) * 1000L,
)
// Login accepts emails; resolve the real account name for /accounts/{handle}.
// The request MUST carry the fresh token (UX report BUG-1: unauthenticated
// /users/me returns 401 and the fallback breaks Channels for email logins).
try {
val authed = plainOkHttp().newBuilder()
.addInterceptor { chain ->
chain.proceed(
chain.request().newBuilder()
.header("Authorization", "Bearer ${token.accessToken}")
.build()
)
}
.build()
val meApi = Retrofit.Builder()
.baseUrl(apiUrlOf(base) + "/")
.client(authed)
.addConverterFactory(json.asConverterFactory("application/json; charset=utf-8".toMediaType()))
.build()
.create(PeerTubeApi::class.java)
store.instanceName = try {
meApi.serverConfig().instance.name
} catch (_: Exception) { null }
val me = meApi.me()
store.accountName = me.username
store.userDisplayName = me.displayName ?: me.username
me.avatars.maxByOrNull { it.width }?.let { avatar ->
store.userAvatarUrl = avatar.fileUrl
?.takeIf { url -> url.startsWith("http") }
?: apiUrlOf(base) + avatar.path
}
} catch (_: Exception) {
store.accountName = username.substringBefore('@') // best effort fallback
}
// Login accepts email addresses too; the login input is not a valid
// handle for /accounts/{handle}, so default it, refreshMyProfile()
// corrects it right after (and on every app start).
store.accountName = username.substringBefore('@')
store.instanceName = runCatching { network.api.serverConfig().instance.name }.getOrNull()
// /users/me fills displayName + avatar; failure is not fatal because
// the profile is refreshed again on the next app start.
refreshMyProfile()
}
suspend fun me(): Result<UserDto> = runCatching { network.api.me() }
suspend fun serverConfig(): Result<ServerConfigDto> = runCatching { network.api.serverConfig() }
/** Display name + avatar URL of the logged-in user, from /users/me. */
data class UserProfile(val displayName: String, val avatarUrl: String?)
/**
* Fetches and persists the profile of the logged-in user. Called after login
* AND on every app start with a stored session: it used to run only at login
* inside a swallow-all try, so sessions from earlier app versions showed the
* raw username and no avatar. Returns null on failure.
*/
suspend fun refreshMyProfile(): UserProfile? {
// /users/me carries displayName + avatars inside `account` (verified
// live v8.3.1); the flat fields exist on some older versions only.
val me = runCatching { network.api.me() }.getOrNull() ?: return null
val name = me.account?.displayName ?: me.displayName ?: me.username
val avatars = me.account?.avatars?.takeIf { it.isNotEmpty() } ?: me.avatars
store.accountName = me.username
val displayName = name
store.userDisplayName = displayName
val avatarUrl = avatars.maxByOrNull { it.width }?.let { avatar ->
avatar.fileUrl?.takeIf { it.startsWith("http") }
// lazy-static avatars live at the instance root, NOT under /api/v1
?: store.baseUrl?.let { base -> normalizeBaseUrl(base) + avatar.path }
}
store.userAvatarUrl = avatarUrl
return UserProfile(displayName, avatarUrl)
}
/** Display name of a (possibly foreign) instance, via that instance's /config. */
suspend fun instanceName(rawHost: String): Result<String> = runCatching {
val base = "https://" + rawHost

View file

@ -165,6 +165,15 @@ data class ChannelStats(
@Serializable
data class UserDto(
val username: String,
// /users/me does NOT return these at top level on newer PeerTube versions —
// the account object carries displayName + avatars (verified live v8.3.1).
val displayName: String? = null,
val avatars: List<Avatar> = emptyList(),
val account: UserAccountDto? = null,
)
@Serializable
data class UserAccountDto(
@SerialName("displayName") val displayName: String? = null,
val avatars: List<Avatar> = emptyList(),
)

View file

@ -19,6 +19,10 @@ class AppViewModel(private val repository: PeerTubeRepository = AppGraph.reposit
private val _sessionState = MutableStateFlow<SessionState>(SessionState.Loading)
val sessionState: StateFlow<SessionState> = _sessionState
/** Display name + avatar of the logged-in user (for the drawer header). */
private val _userProfile = MutableStateFlow<PeerTubeRepository.UserProfile?>(null)
val userProfile: StateFlow<PeerTubeRepository.UserProfile?> = _userProfile
/**
* Bumped on every logout and successful login: all list ViewModels and saved
* UI state are keyed by it, so switching accounts never shows the previous
@ -30,8 +34,19 @@ class AppViewModel(private val repository: PeerTubeRepository = AppGraph.reposit
val loggingIn = MutableStateFlow(false)
init {
_sessionState.value =
if (repository.hasSession()) SessionState.Ready else SessionState.NeedsSetup
if (repository.hasSession()) {
_sessionState.value = SessionState.Ready
// Re-fetch the profile on every app start: stored sessions from older
// app versions may lack displayName/avatar (or the login-time fetch
// failed back then). Failure keeps the stored/fallback values.
refreshProfile()
} else {
_sessionState.value = SessionState.NeedsSetup
}
}
private fun refreshProfile() {
viewModelScope.launch { _userProfile.value = repository.refreshMyProfile() }
}
fun login(baseUrl: String, username: String, password: String) {
@ -43,6 +58,7 @@ class AppViewModel(private val repository: PeerTubeRepository = AppGraph.reposit
if (result.isSuccess) {
_sessionState.value = SessionState.Ready
sessionEpoch.value += 1
refreshProfile()
} else {
loginError.value = if (result.exceptionOrNull() is PeerTubeRepository.BadCredentialsException) {
"Invalid credentials"
@ -67,6 +83,7 @@ class AppViewModel(private val repository: PeerTubeRepository = AppGraph.reposit
/** Clears the stored session; the user returns to the setup screen. */
fun logout() {
repository.signOut()
_userProfile.value = null
_sessionState.value = SessionState.NeedsSetup
sessionEpoch.value += 1
}