mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Fix deleted account issues
Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
@@ -180,10 +180,28 @@
|
||||
<string name="profile_headline_member_since">Дата регистрации</string>
|
||||
<string name="profile_headline_bio">О себе</string>
|
||||
<string name="profile_headline_verification">Официальный аккаунт</string>
|
||||
<string name="profile_verified_support">Это официальный аккаунт</string>
|
||||
<string name="profile_verify_prompt_support">Нажмите, чтобы сделать аккаунт официальным (только админы)</string>
|
||||
<string name="verify">Сделать официальным</string>
|
||||
<string name="unverify">Снять официальный статус</string>
|
||||
<string name="profile_verified_support">Этот аккаунт - официальное лицо FromChat.</string>
|
||||
<string name="profile_admin_actions_category">Действия администратора</string>
|
||||
<string name="profile_admin_verify">Подтвердить</string>
|
||||
<string name="profile_admin_unverify">Отменить подтверждение</string>
|
||||
<string name="profile_admin_suspend">Заблокировать аккаунт</string>
|
||||
<string name="profile_admin_unsuspend">Разблокировать аккаунт</string>
|
||||
<string name="profile_admin_delete">Удалить аккаунт</string>
|
||||
<string name="profile_admin_verify_confirm_title">Подтвердить аккаунт?</string>
|
||||
<string name="profile_admin_verify_confirm_body">Отметить этот аккаунт как официальное лицо FromChat?</string>
|
||||
<string name="profile_admin_unverify_confirm_title">Снять подтверждение?</string>
|
||||
<string name="profile_admin_unverify_confirm_body">Убрать официальное подтверждение с этого аккаунта?</string>
|
||||
<string name="profile_admin_suspend_confirm_title">Заблокировать аккаунт</string>
|
||||
<string name="profile_admin_suspend_confirm_body">Укажите причину блокировки этого аккаунта:</string>
|
||||
<string name="profile_admin_suspend_reason_label">Причина</string>
|
||||
<string name="profile_admin_suspend_confirm">Заблокировать</string>
|
||||
<string name="profile_admin_unsuspend_confirm_title">Разблокировать аккаунт?</string>
|
||||
<string name="profile_admin_unsuspend_confirm_body">Вернуть этому аккаунту возможность отправлять сообщения?</string>
|
||||
<string name="profile_admin_delete_confirm_title">Удалить аккаунт</string>
|
||||
<string name="profile_admin_delete_confirm_body">Данные пользователя будут удалены навсегда, но сообщения и переписки сохранятся. Если пользователь в сети, он будет сразу разлогинен. Это действие нельзя отменить.</string>
|
||||
<string name="profile_admin_delete_confirm">Удалить</string>
|
||||
<string name="verify">Подтвердить</string>
|
||||
<string name="unverify">Отменить подтверждение</string>
|
||||
<string name="cd_verified_account">Официальный аккаунт</string>
|
||||
<string name="cd_account_blocked">Аккаунт заблокирован</string>
|
||||
<string name="cd_similar_verified">Похож на официальный аккаунт</string>
|
||||
|
||||
@@ -198,8 +198,28 @@
|
||||
<string name="profile_headline_member_since">Joined</string>
|
||||
<string name="profile_headline_bio">About</string>
|
||||
<string name="profile_headline_verification">Verified account</string>
|
||||
<string name="profile_verified_support">This account is verified</string>
|
||||
<string name="profile_verify_prompt_support">Tap to verify (admins only)</string>
|
||||
<string name="profile_verified_support">This account is an official FromChat representative.</string>
|
||||
|
||||
<!-- Admin actions (owner / user id 1) -->
|
||||
<string name="profile_admin_actions_category">Admin actions</string>
|
||||
<string name="profile_admin_verify">Verify</string>
|
||||
<string name="profile_admin_unverify">Remove verification</string>
|
||||
<string name="profile_admin_suspend">Suspend account</string>
|
||||
<string name="profile_admin_unsuspend">Unsuspend account</string>
|
||||
<string name="profile_admin_delete">Delete account</string>
|
||||
<string name="profile_admin_verify_confirm_title">Verify account?</string>
|
||||
<string name="profile_admin_verify_confirm_body">Mark this account as an official FromChat representative?</string>
|
||||
<string name="profile_admin_unverify_confirm_title">Remove verification?</string>
|
||||
<string name="profile_admin_unverify_confirm_body">Remove official verification from this account?</string>
|
||||
<string name="profile_admin_suspend_confirm_title">Suspend account</string>
|
||||
<string name="profile_admin_suspend_confirm_body">Enter the reason for suspending this account:</string>
|
||||
<string name="profile_admin_suspend_reason_label">Reason</string>
|
||||
<string name="profile_admin_suspend_confirm">Suspend</string>
|
||||
<string name="profile_admin_unsuspend_confirm_title">Unsuspend account?</string>
|
||||
<string name="profile_admin_unsuspend_confirm_body">Restore this account’s ability to send messages?</string>
|
||||
<string name="profile_admin_delete_confirm_title">Delete account</string>
|
||||
<string name="profile_admin_delete_confirm_body">This will permanently delete user data but preserve messages and conversations. If the user is online, they will be immediately logged out. This action cannot be undone.</string>
|
||||
<string name="profile_admin_delete_confirm">Delete</string>
|
||||
|
||||
<!-- Verify (admin) -->
|
||||
<string name="verify">Verify</string>
|
||||
|
||||
@@ -108,6 +108,7 @@ import ru.fromchat.api.schema.user.devices.DevicesListResponse
|
||||
import ru.fromchat.api.schema.user.keys.BackupBlobRequest
|
||||
import ru.fromchat.api.schema.user.keys.BackupBlobResponse
|
||||
import ru.fromchat.api.schema.user.keys.PublicKeyResponse
|
||||
import ru.fromchat.api.schema.user.profile.SuspendUserRequest
|
||||
import ru.fromchat.api.schema.user.profile.UpdateProfileRequest
|
||||
import ru.fromchat.api.schema.user.profile.UpdateProfileResponse
|
||||
import ru.fromchat.api.schema.user.profile.UserProfile
|
||||
@@ -637,6 +638,34 @@ object ApiClient {
|
||||
.body<VerifyResponse>()
|
||||
}.getOrNull()
|
||||
|
||||
suspend fun suspendUser(userId: Int, reason: String): SimpleStatusResponse? =
|
||||
runCatching {
|
||||
http
|
||||
.post("${ServerConfig.apiBaseUrl}/user/$userId/suspend") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(SuspendUserRequest(reason = reason))
|
||||
}
|
||||
.body<SimpleStatusResponse>()
|
||||
}.getOrNull()
|
||||
|
||||
suspend fun unsuspendUser(userId: Int): SimpleStatusResponse? =
|
||||
runCatching {
|
||||
http
|
||||
.post("${ServerConfig.apiBaseUrl}/user/$userId/unsuspend") {
|
||||
contentType(ContentType.Application.Json)
|
||||
}
|
||||
.body<SimpleStatusResponse>()
|
||||
}.getOrNull()
|
||||
|
||||
suspend fun adminDeleteUser(userId: Int): SimpleStatusResponse? =
|
||||
runCatching {
|
||||
http
|
||||
.post("${ServerConfig.apiBaseUrl}/user/$userId/delete") {
|
||||
contentType(ContentType.Application.Json)
|
||||
}
|
||||
.body<SimpleStatusResponse>()
|
||||
}.getOrNull()
|
||||
|
||||
suspend fun getDmConversations(): List<DmConversation> =
|
||||
http
|
||||
.get("${ServerConfig.apiBaseUrl}/dm/conversations") {
|
||||
@@ -1589,9 +1618,7 @@ object ApiClient {
|
||||
|
||||
suspend fun deleteMessage(messageId: Int) {
|
||||
if (_suspensionState.value.isSuspended) return
|
||||
runCatching {
|
||||
http.delete("${ServerConfig.apiBaseUrl}/delete_message/$messageId")
|
||||
}
|
||||
// WS-only (like Web): HTTP-first would hard-delete before WS and skip messageDeleted broadcast.
|
||||
WebSocketManager.send(
|
||||
WebSocketMessage(
|
||||
type = "deleteMessage",
|
||||
|
||||
@@ -297,6 +297,10 @@ object MessageCacheStore {
|
||||
syncDmConversationPreviewFromCache(otherUserId)
|
||||
}
|
||||
|
||||
suspend fun deletePublicMessageById(messageId: Int) {
|
||||
deleteMessageById(conversationIdForPublic(), messageId)
|
||||
}
|
||||
|
||||
suspend fun deleteMessageByClientMessageId(conversationId: String, clientMessageId: String) {
|
||||
deleteByClientMessageId(conversationId, clientMessageId)
|
||||
}
|
||||
|
||||
@@ -73,6 +73,9 @@ object MessageRepository {
|
||||
suspend fun markPublicMessageDeleted(messageId: Int) =
|
||||
markMessageDeleted(conversationIdForGroup(GENERAL_PUBLIC_GROUP_ID), messageId)
|
||||
|
||||
suspend fun deletePublicMessageById(messageId: Int) =
|
||||
MessageCacheStore.deletePublicMessageById(messageId)
|
||||
|
||||
suspend fun loadDmMessages(otherUserId: Int): List<Message> =
|
||||
MessageCacheStore.loadDmMessages(otherUserId)
|
||||
|
||||
|
||||
@@ -26,7 +26,11 @@ import kotlin.concurrent.Volatile
|
||||
* to the UI (for suspended or deleted users), except for the current user.
|
||||
*/
|
||||
fun UserProfile.shouldHideUsername(currentUserId: Int? = null): Boolean =
|
||||
id != currentUserId && (deleted == true || isDeletedPlaceholderUsername(username))
|
||||
id != currentUserId && (
|
||||
deleted == true ||
|
||||
isDeletedPlaceholderUsername(username) ||
|
||||
(suspended == true && currentUserId != 1)
|
||||
)
|
||||
|
||||
private fun isDeletedPlaceholderUsername(username: String?): Boolean =
|
||||
username?.startsWith("#deleted") == true
|
||||
|
||||
@@ -34,12 +34,11 @@ object UserStatusStore {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeTyping(userId: Int, username: String) {
|
||||
val trimmed = username.trim().takeIf { it.isNotBlank() } ?: return
|
||||
fun removeTyping(userId: Int, username: String = "") {
|
||||
_status.update { current ->
|
||||
val existing = current[userId] ?: return@update current
|
||||
val typing = existing.typingUsernames.filterNot { it.equals(trimmed, ignoreCase = true) }
|
||||
current + (userId to existing.copy(typingUsernames = typing))
|
||||
// Clear by userId: after suspend/delete the stop event may use #deleted{id}.
|
||||
current + (userId to existing.copy(typingUsernames = emptyList()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package ru.fromchat.api.schema.user.profile
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SuspendUserRequest(
|
||||
val reason: String,
|
||||
)
|
||||
@@ -71,11 +71,6 @@ import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import kotlinx.datetime.LocalDate
|
||||
import ru.fromchat.api.local.messages.formatChatDateSeparator
|
||||
import ru.fromchat.chat_date_today
|
||||
@@ -200,49 +195,9 @@ fun ChatScreen(
|
||||
LazyListState(0, 0)
|
||||
}
|
||||
val fakeScrollOffset = remember(panelId) { Animatable(0f) }
|
||||
LaunchedEffect(panelId) {
|
||||
// #region agent log
|
||||
agentDebugLog(
|
||||
hypothesisId = "H4",
|
||||
location = "ChatScreen.kt:188",
|
||||
message = "chat_screen_opened",
|
||||
data = mapOf(
|
||||
"panelId" to panelId,
|
||||
"listStateHash" to listState.hashCode(),
|
||||
),
|
||||
)
|
||||
// #endregion
|
||||
}
|
||||
val isNearBottom by remember(panelId) {
|
||||
derivedStateOf { listState.isChatNearBottom() }
|
||||
}
|
||||
LaunchedEffect(listState, panelId) {
|
||||
snapshotFlow {
|
||||
val info = listState.layoutInfo
|
||||
Triple(
|
||||
info.totalItemsCount,
|
||||
listState.firstVisibleItemIndex,
|
||||
listState.firstVisibleItemScrollOffset,
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collect { (totalItems, firstIndex, firstOffset) ->
|
||||
// #region agent log
|
||||
agentDebugLog(
|
||||
hypothesisId = "H1",
|
||||
location = "ChatScreen.kt:196",
|
||||
message = "scroll_state_changed",
|
||||
data = mapOf(
|
||||
"panelId" to panelId,
|
||||
"totalItems" to totalItems,
|
||||
"firstVisibleItemIndex" to firstIndex,
|
||||
"firstVisibleItemScrollOffset" to firstOffset,
|
||||
"isNearBottom" to listState.isChatNearBottom(),
|
||||
),
|
||||
)
|
||||
// #endregion
|
||||
}
|
||||
}
|
||||
val density = LocalDensity.current
|
||||
val fallbackMessageHeightPx = remember(density) { with(density) { 80.dp.roundToPx() } }
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -485,6 +440,7 @@ fun ChatScreen(
|
||||
userId = userId,
|
||||
currentUserId = currentUserId,
|
||||
deleted = profile.deleted,
|
||||
suspended = profile.suspended,
|
||||
username = profile.username,
|
||||
)
|
||||
}
|
||||
@@ -1361,23 +1317,6 @@ fun ChatScreen(
|
||||
val showScrollToBottomFab = !isNearBottom &&
|
||||
panelState.messages.isNotEmpty() &&
|
||||
!contextMenuState.isOpen
|
||||
LaunchedEffect(showScrollToBottomFab) {
|
||||
// #region agent log
|
||||
agentDebugLog(
|
||||
hypothesisId = "H2",
|
||||
location = "ChatScreen.kt:1338",
|
||||
message = "show_scroll_button_changed",
|
||||
data = mapOf(
|
||||
"panelId" to panelId,
|
||||
"showScrollToBottomFab" to showScrollToBottomFab,
|
||||
"isNearBottom" to isNearBottom,
|
||||
"messageCount" to panelState.messages.size,
|
||||
"contextMenuOpen" to contextMenuState.isOpen,
|
||||
"usesPublicGroupSubtitle" to panel.usesPublicGroupSubtitle,
|
||||
),
|
||||
)
|
||||
// #endregion
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = showScrollToBottomFab,
|
||||
enter = fadeIn(
|
||||
@@ -1415,19 +1354,6 @@ fun ChatScreen(
|
||||
ChatScrollToBottomButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
// #region agent log
|
||||
agentDebugLog(
|
||||
hypothesisId = "H5",
|
||||
location = "ChatScreen.kt:1402",
|
||||
message = "scroll_button_jump_to_bottom",
|
||||
data = mapOf(
|
||||
"panelId" to panelId,
|
||||
"fromIndex" to listState.firstVisibleItemIndex,
|
||||
"fromOffset" to listState.firstVisibleItemScrollOffset,
|
||||
),
|
||||
)
|
||||
// #endregion
|
||||
|
||||
// Snap list to bottom first so content is correct.
|
||||
listState.scrollChatToBottom()
|
||||
|
||||
@@ -1648,16 +1574,6 @@ private fun ChatScrollToBottomButton(
|
||||
.size(ChatScrollToBottomButtonSize)
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
// #region agent log
|
||||
agentDebugLog(
|
||||
hypothesisId = "H3",
|
||||
location = "ChatScreen.kt:1519",
|
||||
message = "scroll_button_clicked",
|
||||
data = mapOf(
|
||||
"contentDescription" to contentDescription,
|
||||
),
|
||||
)
|
||||
// #endregion
|
||||
onClick()
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
@@ -1677,70 +1593,6 @@ private fun ChatScrollToBottomButton(
|
||||
}
|
||||
}
|
||||
|
||||
private fun agentDebugLog(
|
||||
hypothesisId: String,
|
||||
location: String,
|
||||
message: String,
|
||||
data: Map<String, Any?> = emptyMap(),
|
||||
runId: String = "initial",
|
||||
) {
|
||||
val timestamp = kotlin.time.Clock.System.now().toEpochMilliseconds()
|
||||
fun escape(value: String): String {
|
||||
return value
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
}
|
||||
|
||||
val dataJson = buildString {
|
||||
append("{")
|
||||
data.entries.joinToString(",") { (key, rawValue) ->
|
||||
val value = rawValue?.toString() ?: "null"
|
||||
"\"${escape(key)}\":\"${escape(value)}\""
|
||||
}.let { append(it) }
|
||||
append("}")
|
||||
}
|
||||
|
||||
val json = buildString {
|
||||
append("{")
|
||||
append("\"sessionId\":\"a042f7\",")
|
||||
append("\"id\":\"log_")
|
||||
append(timestamp)
|
||||
append("_")
|
||||
append(escape(hypothesisId))
|
||||
append("\",")
|
||||
append("\"timestamp\":")
|
||||
append(timestamp)
|
||||
append(",")
|
||||
append("\"location\":\"")
|
||||
append(escape(location))
|
||||
append("\",")
|
||||
append("\"message\":\"")
|
||||
append(escape(message))
|
||||
append("\",")
|
||||
append("\"runId\":\"")
|
||||
append(escape(runId))
|
||||
append("\",")
|
||||
append("\"hypothesisId\":\"")
|
||||
append(escape(hypothesisId))
|
||||
append("\",")
|
||||
append("\"data\":")
|
||||
append(dataJson)
|
||||
append("}")
|
||||
}
|
||||
|
||||
Logger.d("AgentDebug", json)
|
||||
|
||||
kotlinx.coroutines.GlobalScope.launch {
|
||||
runCatching {
|
||||
ApiClient.http.post("http://192.168.1.6:7629/ingest/edf4b1c4-ae73-4d46-9110-6235fc587a70") {
|
||||
contentType(ContentType.Application.Json)
|
||||
header("X-Debug-Session-Id", "a042f7")
|
||||
setBody(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun LazyListState.scrollChatMessageToCenter(
|
||||
lazyIndex: Int,
|
||||
topClearancePx: Int,
|
||||
|
||||
@@ -10,8 +10,8 @@ import ru.fromchat.api.local.db.store.visibleDisplayName
|
||||
import ru.fromchat.ui.profile.avatarLabelForInitials
|
||||
import ru.fromchat.message_sender_you
|
||||
import ru.fromchat.ui.profile.deletedUserDisplayNameForUi
|
||||
import ru.fromchat.ui.profile.isDeletedAccount
|
||||
import ru.fromchat.ui.profile.isDeletedAccountUsername
|
||||
import ru.fromchat.ui.profile.isRedactedPeerAccount
|
||||
import ru.fromchat.ui.profile.peerIsDeleted
|
||||
|
||||
/**
|
||||
@@ -23,7 +23,7 @@ fun messageDisplayUsername(message: Message, currentUserId: Int?): String {
|
||||
return stringResource(Res.string.message_sender_you)
|
||||
}
|
||||
ProfileCache.get(message.user_id)?.let { profile ->
|
||||
if (profile.isDeletedAccount(currentUserId) || isDeletedAccountUsername(profile.username)) {
|
||||
if (profile.isRedactedPeerAccount(currentUserId)) {
|
||||
return deletedUserDisplayNameForUi()
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,10 @@ fun messageSenderProfilePicture(
|
||||
message: Message,
|
||||
currentUserId: Int? = ApiClient.user?.id,
|
||||
): String? {
|
||||
if (ProfileCache.get(message.user_id)?.isDeletedAccount(currentUserId) == true) {
|
||||
if (ProfileCache.get(message.user_id)?.isRedactedPeerAccount(currentUserId) == true) {
|
||||
return null
|
||||
}
|
||||
if (isDeletedAccountUsername(message.username)) {
|
||||
return null
|
||||
}
|
||||
if (currentUserId != null && message.user_id == currentUserId) {
|
||||
|
||||
@@ -30,7 +30,7 @@ import ru.fromchat.api.schema.messages.dm.DmEnvelope
|
||||
import ru.fromchat.api.schema.websocket.WebSocketMessage
|
||||
import ru.fromchat.api.schema.websocket.types.DmDeletedData
|
||||
import ru.fromchat.ui.profile.displayNameText
|
||||
import ru.fromchat.ui.profile.isDeletedAccount
|
||||
import ru.fromchat.ui.profile.isRedactedPeerAccount
|
||||
import ru.fromchat.Logger
|
||||
import ru.fromchat.config.ServerConfig
|
||||
import ru.fromchat.api.crypto.CorruptedDmMessagePlaceholder
|
||||
@@ -120,7 +120,7 @@ class DmPanel(
|
||||
try {
|
||||
val profile = ApiClient.getProfileById(otherUserId)
|
||||
if (
|
||||
!profile.isDeletedAccount(ApiClient.user?.id) &&
|
||||
!profile.isRedactedPeerAccount(ApiClient.user?.id) &&
|
||||
profile.username.isBlank() &&
|
||||
profile.displayName.isNullOrBlank()
|
||||
) {
|
||||
|
||||
+12
-5
@@ -274,7 +274,12 @@ class PublicChatPanel(
|
||||
mergeMessageUiFields(net, local)
|
||||
}
|
||||
}
|
||||
val ahead = shown.filter { it.id > 0 && it.id !in networkIds }
|
||||
val minNetworkId = fromNetwork.minOfOrNull { it.id } ?: Int.MAX_VALUE
|
||||
val maxNetworkId = fromNetwork.maxOfOrNull { it.id } ?: Int.MIN_VALUE
|
||||
// Keep only messages strictly newer/older than the network page — not in-window gaps
|
||||
// (those are server-side deletes that must not be resurrected from cache).
|
||||
val ahead = shown.filter { it.id > 0 && it.id !in networkIds && it.id > maxNetworkId }
|
||||
val older = shown.filter { it.id > 0 && it.id !in networkIds && it.id < minNetworkId }
|
||||
val inFlight = shown.filter { msg ->
|
||||
msg.id < 0 && (
|
||||
!msg.client_message_id.isNullOrBlank() ||
|
||||
@@ -282,8 +287,8 @@ class PublicChatPanel(
|
||||
!msg.uploadJobId.isNullOrBlank()
|
||||
)
|
||||
}
|
||||
if (ahead.isEmpty() && inFlight.isEmpty()) return merged
|
||||
val combined = merged + ahead + inFlight
|
||||
if (ahead.isEmpty() && older.isEmpty() && inFlight.isEmpty()) return merged
|
||||
val combined = merged + ahead + older + inFlight
|
||||
return ru.fromchat.api.local.messages.sortMessagesForChatDisplay(
|
||||
ru.fromchat.ui.chat.utils.dedupeMessagesByClientId(combined),
|
||||
)
|
||||
@@ -493,8 +498,7 @@ class PublicChatPanel(
|
||||
removeMessage(deletedData.message_id)
|
||||
clearReplyReferencesTo(deletedData.message_id)
|
||||
withContext(Dispatchers.Default) {
|
||||
MessageRepository.markPublicMessageDeleted(deletedData.message_id)
|
||||
MessageCacheStore.replacePublicMessages(_state.messages)
|
||||
MessageRepository.deletePublicMessageById(deletedData.message_id)
|
||||
}
|
||||
}
|
||||
"reactionUpdate" -> {
|
||||
@@ -572,6 +576,9 @@ class PublicChatPanel(
|
||||
return
|
||||
}
|
||||
beginMessageDissolve(message)
|
||||
withContext(Dispatchers.Default) {
|
||||
MessageRepository.deletePublicMessageById(messageId)
|
||||
}
|
||||
ApiClient.deleteMessage(messageId)
|
||||
}
|
||||
|
||||
|
||||
@@ -459,6 +459,7 @@ internal fun SearchConversationsList(
|
||||
userId = user.id,
|
||||
currentUserId = ApiClient.user?.id,
|
||||
deleted = user.deleted ?: cached?.deleted,
|
||||
suspended = user.suspended ?: cached?.suspended,
|
||||
username = user.username,
|
||||
)
|
||||
val avatarUrl = if (isPeerDeleted) null else cached?.profilePicture ?: user.profile_picture
|
||||
@@ -926,6 +927,7 @@ internal fun DmConversationRowContent(
|
||||
userId = conversation.otherUserId,
|
||||
currentUserId = currentUserId,
|
||||
deleted = cached?.deleted,
|
||||
suspended = cached?.suspended,
|
||||
username = cached?.username ?: conversation.displayName.takeIf { it.isNotBlank() },
|
||||
)
|
||||
val avatarUrl = if (isPeerDeleted) null else cached?.profilePicture
|
||||
|
||||
@@ -69,7 +69,10 @@ import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||
import androidx.compose.material.icons.filled.AlternateEmail
|
||||
import androidx.compose.material.icons.filled.Block
|
||||
import androidx.compose.material.icons.filled.CalendarMonth
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.DeleteForever
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
@@ -79,11 +82,14 @@ import androidx.compose.material.icons.filled.Verified
|
||||
import androidx.compose.material.icons.rounded.Call
|
||||
import androidx.compose.material.icons.rounded.ContentCopy
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -137,6 +143,8 @@ import ru.fromchat.api.local.db.store.UserStatusStore
|
||||
import ru.fromchat.api.local.db.store.visibleDisplayName
|
||||
import ru.fromchat.api.local.db.store.visibleUsername
|
||||
import ru.fromchat.api.schema.user.profile.UserProfile
|
||||
import ru.fromchat.api.schema.user.profile.VerificationStatus
|
||||
import ru.fromchat.cancel
|
||||
import ru.fromchat.config.ServerConfig
|
||||
import ru.fromchat.feature_not_implemented
|
||||
import ru.fromchat.presence_online
|
||||
@@ -146,6 +154,25 @@ import ru.fromchat.profile_action_chat
|
||||
import ru.fromchat.profile_action_link
|
||||
import ru.fromchat.profile_action_search
|
||||
import ru.fromchat.profile_action_settings
|
||||
import ru.fromchat.profile_admin_actions_category
|
||||
import ru.fromchat.profile_admin_delete
|
||||
import ru.fromchat.profile_admin_delete_confirm
|
||||
import ru.fromchat.profile_admin_delete_confirm_body
|
||||
import ru.fromchat.profile_admin_delete_confirm_title
|
||||
import ru.fromchat.profile_admin_suspend
|
||||
import ru.fromchat.profile_admin_suspend_confirm
|
||||
import ru.fromchat.profile_admin_suspend_confirm_body
|
||||
import ru.fromchat.profile_admin_suspend_confirm_title
|
||||
import ru.fromchat.profile_admin_suspend_reason_label
|
||||
import ru.fromchat.profile_admin_unsuspend
|
||||
import ru.fromchat.profile_admin_unsuspend_confirm_body
|
||||
import ru.fromchat.profile_admin_unsuspend_confirm_title
|
||||
import ru.fromchat.profile_admin_unverify
|
||||
import ru.fromchat.profile_admin_unverify_confirm_body
|
||||
import ru.fromchat.profile_admin_unverify_confirm_title
|
||||
import ru.fromchat.profile_admin_verify
|
||||
import ru.fromchat.profile_admin_verify_confirm_body
|
||||
import ru.fromchat.profile_admin_verify_confirm_title
|
||||
import ru.fromchat.profile_headline_bio
|
||||
import ru.fromchat.profile_headline_member_since
|
||||
import ru.fromchat.profile_headline_username
|
||||
@@ -153,7 +180,6 @@ import ru.fromchat.profile_headline_verification
|
||||
import ru.fromchat.profile_load_failed
|
||||
import ru.fromchat.profile_not_found
|
||||
import ru.fromchat.profile_verified_support
|
||||
import ru.fromchat.profile_verify_prompt_support
|
||||
import ru.fromchat.ui.profile.deletedUserDisplayNameForUi
|
||||
import ru.fromchat.ui.profile.avatarLabelForInitials
|
||||
import ru.fromchat.ui.profile.displayNameForUi
|
||||
@@ -501,7 +527,12 @@ fun ProfileScreen(
|
||||
val headlineBio = stringResource(Res.string.profile_headline_bio)
|
||||
val headlineVerification = stringResource(Res.string.profile_headline_verification)
|
||||
val verifiedSupport = stringResource(Res.string.profile_verified_support)
|
||||
val verifyPromptSupport = stringResource(Res.string.profile_verify_prompt_support)
|
||||
val adminActionsCategory = stringResource(Res.string.profile_admin_actions_category)
|
||||
val adminVerifyLabel = stringResource(Res.string.profile_admin_verify)
|
||||
val adminUnverifyLabel = stringResource(Res.string.profile_admin_unverify)
|
||||
val adminSuspendLabel = stringResource(Res.string.profile_admin_suspend)
|
||||
val adminUnsuspendLabel = stringResource(Res.string.profile_admin_unsuspend)
|
||||
val adminDeleteLabel = stringResource(Res.string.profile_admin_delete)
|
||||
|
||||
val profile = liveProfile
|
||||
?: state.profile
|
||||
@@ -531,6 +562,7 @@ fun ProfileScreen(
|
||||
userId = resolvedUserId,
|
||||
currentUserId = viewerUserId,
|
||||
deleted = resolvedProfile?.deleted,
|
||||
suspended = resolvedProfile?.suspended,
|
||||
username = resolvedProfile?.username,
|
||||
)
|
||||
val displayName = when {
|
||||
@@ -655,9 +687,11 @@ fun ProfileScreen(
|
||||
val showDetailsUsername = usernameForLinks != null
|
||||
val showDetailsMemberSince = !resolvedProfile?.createdAt.isNullOrBlank()
|
||||
val showDetailsBio = !resolvedProfile?.bio.isNullOrBlank()
|
||||
val showDetailsVerify = !isDeletedProfile && (
|
||||
resolvedProfile?.verified == true || ApiClient.user?.id == 1
|
||||
)
|
||||
val showDetailsVerify = !isDeletedProfile && resolvedProfile?.verified == true
|
||||
val showAdminActions = !isOwnProfile &&
|
||||
!isDeletedProfile &&
|
||||
resolvedProfile != null &&
|
||||
ApiClient.user?.id == 1
|
||||
val showDetailsSection = resolvedProfile != null && !isDeletedProfile && (
|
||||
showDetailsUsername || showDetailsMemberSince || showDetailsBio || showDetailsVerify
|
||||
)
|
||||
@@ -779,13 +813,19 @@ fun ProfileScreen(
|
||||
showDetailsMemberSince = showDetailsMemberSince,
|
||||
showDetailsBio = showDetailsBio,
|
||||
showDetailsVerify = showDetailsVerify,
|
||||
showAdminActions = showAdminActions,
|
||||
headlineUsername = headlineUsername,
|
||||
headlineMemberSince = headlineMemberSince,
|
||||
headlineBio = headlineBio,
|
||||
headlineVerification = headlineVerification,
|
||||
usernameForLinks = usernameForLinks,
|
||||
verifiedSupport = verifiedSupport,
|
||||
verifyPromptSupport = verifyPromptSupport,
|
||||
adminActionsCategory = adminActionsCategory,
|
||||
adminVerifyLabel = adminVerifyLabel,
|
||||
adminUnverifyLabel = adminUnverifyLabel,
|
||||
adminSuspendLabel = adminSuspendLabel,
|
||||
adminUnsuspendLabel = adminUnsuspendLabel,
|
||||
adminDeleteLabel = adminDeleteLabel,
|
||||
registrationDateStrings = registrationDateStrings,
|
||||
listItemIconTint = listItemIconTint,
|
||||
labelCopy = labelCopy,
|
||||
@@ -796,6 +836,7 @@ fun ProfileScreen(
|
||||
navController = navController,
|
||||
scope = scope,
|
||||
openContextMenuHaptic = openContextMenuHaptic,
|
||||
onBack = onBack,
|
||||
onProfileUpdated = { updated ->
|
||||
state = state.copy(profile = updated)
|
||||
ProfileCache.put(updated)
|
||||
@@ -1261,13 +1302,19 @@ private fun ProfileLoadedBody(
|
||||
showDetailsMemberSince: Boolean,
|
||||
showDetailsBio: Boolean,
|
||||
showDetailsVerify: Boolean,
|
||||
showAdminActions: Boolean,
|
||||
headlineUsername: String,
|
||||
headlineMemberSince: String,
|
||||
headlineBio: String,
|
||||
headlineVerification: String,
|
||||
usernameForLinks: String?,
|
||||
verifiedSupport: String,
|
||||
verifyPromptSupport: String,
|
||||
adminActionsCategory: String,
|
||||
adminVerifyLabel: String,
|
||||
adminUnverifyLabel: String,
|
||||
adminSuspendLabel: String,
|
||||
adminUnsuspendLabel: String,
|
||||
adminDeleteLabel: String,
|
||||
registrationDateStrings: RegistrationDateFormatStrings,
|
||||
listItemIconTint: Color,
|
||||
labelCopy: String,
|
||||
@@ -1278,9 +1325,17 @@ private fun ProfileLoadedBody(
|
||||
navController: NavController,
|
||||
scope: CoroutineScope,
|
||||
openContextMenuHaptic: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onProfileUpdated: (UserProfile) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showVerifyConfirm by remember { mutableStateOf(false) }
|
||||
var showUnverifyConfirm by remember { mutableStateOf(false) }
|
||||
var showSuspendConfirm by remember { mutableStateOf(false) }
|
||||
var showUnsuspendConfirm by remember { mutableStateOf(false) }
|
||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||
var suspendReason by remember { mutableStateOf("") }
|
||||
var adminActionInProgress by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -1486,11 +1541,7 @@ private fun ProfileLoadedBody(
|
||||
detailIndex++
|
||||
ListItem(
|
||||
headline = headlineVerification,
|
||||
supportingText = if (resolvedProfile.verified == true) {
|
||||
verifiedSupport
|
||||
} else {
|
||||
verifyPromptSupport
|
||||
},
|
||||
supportingText = verifiedSupport,
|
||||
divider = true,
|
||||
position = position,
|
||||
groupItemCount = detailCount,
|
||||
@@ -1501,36 +1552,312 @@ private fun ProfileLoadedBody(
|
||||
tint = listItemIconTint,
|
||||
)
|
||||
},
|
||||
onClick = if (ApiClient.user?.id == 1) {
|
||||
{
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAdminActions) {
|
||||
val isVerified = resolvedProfile.verified == true
|
||||
val isSuspended = resolvedProfile.suspended == true
|
||||
Category(
|
||||
title = adminActionsCategory,
|
||||
margin = PaddingValues(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = if (showDetailsSection) 8.dp else 28.dp,
|
||||
bottom = 20.dp,
|
||||
),
|
||||
roundedCorners = false,
|
||||
) {
|
||||
ListItem(
|
||||
headline = if (isVerified) adminUnverifyLabel else adminVerifyLabel,
|
||||
divider = true,
|
||||
position = ListItemPosition.START,
|
||||
groupItemCount = 3,
|
||||
enabled = !adminActionInProgress,
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Verified,
|
||||
contentDescription = null,
|
||||
tint = listItemIconTint,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
if (isVerified) showUnverifyConfirm = true else showVerifyConfirm = true
|
||||
},
|
||||
)
|
||||
ListItem(
|
||||
headline = if (isSuspended) adminUnsuspendLabel else adminSuspendLabel,
|
||||
divider = true,
|
||||
position = ListItemPosition.MIDDLE,
|
||||
groupItemCount = 3,
|
||||
enabled = !adminActionInProgress,
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = if (isSuspended) {
|
||||
Icons.Filled.CheckCircle
|
||||
} else {
|
||||
Icons.Filled.Block
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
if (isSuspended) {
|
||||
showUnsuspendConfirm = true
|
||||
} else {
|
||||
suspendReason = ""
|
||||
showSuspendConfirm = true
|
||||
}
|
||||
},
|
||||
)
|
||||
ListItem(
|
||||
headline = adminDeleteLabel,
|
||||
divider = true,
|
||||
position = ListItemPosition.END,
|
||||
groupItemCount = 3,
|
||||
enabled = !adminActionInProgress,
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.DeleteForever,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
onClick = { showDeleteConfirm = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showVerifyConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!adminActionInProgress) showVerifyConfirm = false },
|
||||
title = { Text(stringResource(Res.string.profile_admin_verify_confirm_title)) },
|
||||
text = { Text(stringResource(Res.string.profile_admin_verify_confirm_body)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = !adminActionInProgress,
|
||||
onClick = {
|
||||
adminActionInProgress = true
|
||||
scope.launch {
|
||||
val result = withContext(Dispatchers.Default) {
|
||||
runCatching {
|
||||
ApiClient.verifyUser(resolvedProfile.id)
|
||||
}.getOrNull()
|
||||
runCatching { ApiClient.verifyUser(resolvedProfile.id) }.getOrNull()
|
||||
}
|
||||
adminActionInProgress = false
|
||||
showVerifyConfirm = false
|
||||
result?.let { response ->
|
||||
onProfileUpdated(
|
||||
resolvedProfile.copy(
|
||||
verified = response.verified,
|
||||
verificationStatus = response.verificationStatus
|
||||
?: response.verified.let { verified ->
|
||||
if (verified) {
|
||||
ru.fromchat.api.schema.user.profile.VerificationStatus.Verified
|
||||
?: if (response.verified) {
|
||||
VerificationStatus.Verified
|
||||
} else {
|
||||
ru.fromchat.api.schema.user.profile.VerificationStatus.None
|
||||
}
|
||||
VerificationStatus.None
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(Res.string.profile_admin_verify))
|
||||
}
|
||||
} else null,
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
enabled = !adminActionInProgress,
|
||||
onClick = { showVerifyConfirm = false },
|
||||
) {
|
||||
Text(stringResource(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showUnverifyConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!adminActionInProgress) showUnverifyConfirm = false },
|
||||
title = { Text(stringResource(Res.string.profile_admin_unverify_confirm_title)) },
|
||||
text = { Text(stringResource(Res.string.profile_admin_unverify_confirm_body)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = !adminActionInProgress,
|
||||
onClick = {
|
||||
adminActionInProgress = true
|
||||
scope.launch {
|
||||
val result = withContext(Dispatchers.Default) {
|
||||
runCatching { ApiClient.verifyUser(resolvedProfile.id) }.getOrNull()
|
||||
}
|
||||
adminActionInProgress = false
|
||||
showUnverifyConfirm = false
|
||||
result?.let { response ->
|
||||
onProfileUpdated(
|
||||
resolvedProfile.copy(
|
||||
verified = response.verified,
|
||||
verificationStatus = response.verificationStatus
|
||||
?: if (response.verified) {
|
||||
VerificationStatus.Verified
|
||||
} else {
|
||||
VerificationStatus.None
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(Res.string.profile_admin_unverify))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
enabled = !adminActionInProgress,
|
||||
onClick = { showUnverifyConfirm = false },
|
||||
) {
|
||||
Text(stringResource(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showSuspendConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!adminActionInProgress) showSuspendConfirm = false },
|
||||
title = { Text(stringResource(Res.string.profile_admin_suspend_confirm_title)) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(stringResource(Res.string.profile_admin_suspend_confirm_body))
|
||||
OutlinedTextField(
|
||||
value = suspendReason,
|
||||
onValueChange = { suspendReason = it },
|
||||
label = { Text(stringResource(Res.string.profile_admin_suspend_reason_label)) },
|
||||
enabled = !adminActionInProgress,
|
||||
singleLine = false,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = !adminActionInProgress && suspendReason.trim().isNotEmpty(),
|
||||
onClick = {
|
||||
val reason = suspendReason.trim()
|
||||
adminActionInProgress = true
|
||||
scope.launch {
|
||||
val result = withContext(Dispatchers.Default) {
|
||||
runCatching {
|
||||
ApiClient.suspendUser(resolvedProfile.id, reason)
|
||||
}.getOrNull()
|
||||
}
|
||||
adminActionInProgress = false
|
||||
showSuspendConfirm = false
|
||||
if (result != null) {
|
||||
onProfileUpdated(
|
||||
resolvedProfile.copy(
|
||||
suspended = true,
|
||||
suspensionReason = reason,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(Res.string.profile_admin_suspend_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
enabled = !adminActionInProgress,
|
||||
onClick = { showSuspendConfirm = false },
|
||||
) {
|
||||
Text(stringResource(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showUnsuspendConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!adminActionInProgress) showUnsuspendConfirm = false },
|
||||
title = { Text(stringResource(Res.string.profile_admin_unsuspend_confirm_title)) },
|
||||
text = { Text(stringResource(Res.string.profile_admin_unsuspend_confirm_body)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = !adminActionInProgress,
|
||||
onClick = {
|
||||
adminActionInProgress = true
|
||||
scope.launch {
|
||||
val result = withContext(Dispatchers.Default) {
|
||||
runCatching { ApiClient.unsuspendUser(resolvedProfile.id) }.getOrNull()
|
||||
}
|
||||
adminActionInProgress = false
|
||||
showUnsuspendConfirm = false
|
||||
if (result != null) {
|
||||
onProfileUpdated(
|
||||
resolvedProfile.copy(
|
||||
suspended = false,
|
||||
suspensionReason = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(Res.string.profile_admin_unsuspend))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
enabled = !adminActionInProgress,
|
||||
onClick = { showUnsuspendConfirm = false },
|
||||
) {
|
||||
Text(stringResource(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (showDeleteConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!adminActionInProgress) showDeleteConfirm = false },
|
||||
title = { Text(stringResource(Res.string.profile_admin_delete_confirm_title)) },
|
||||
text = { Text(stringResource(Res.string.profile_admin_delete_confirm_body)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = !adminActionInProgress,
|
||||
onClick = {
|
||||
adminActionInProgress = true
|
||||
scope.launch {
|
||||
val result = withContext(Dispatchers.Default) {
|
||||
runCatching { ApiClient.adminDeleteUser(resolvedProfile.id) }.getOrNull()
|
||||
}
|
||||
adminActionInProgress = false
|
||||
showDeleteConfirm = false
|
||||
if (result != null) {
|
||||
onProfileUpdated(
|
||||
resolvedProfile.copy(deleted = true),
|
||||
)
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(Res.string.profile_admin_delete_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
enabled = !adminActionInProgress,
|
||||
onClick = { showDeleteConfirm = false },
|
||||
) {
|
||||
Text(stringResource(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,14 @@ fun UserProfile.isDeletedAccount(currentUserId: Int? = null): Boolean =
|
||||
fun UserProfile.isSuspendedAccount(currentUserId: Int? = null): Boolean =
|
||||
suspended == true && deleted != true && id != currentUserId
|
||||
|
||||
/** True when peers should see this profile as a deleted/hidden account. */
|
||||
fun UserProfile.isRedactedPeerAccount(currentUserId: Int? = null): Boolean =
|
||||
id != currentUserId && (
|
||||
deleted == true ||
|
||||
isDeletedAccountUsername(username) ||
|
||||
(suspended == true && currentUserId != 1)
|
||||
)
|
||||
|
||||
fun isDeletedAccountUsername(username: String?): Boolean =
|
||||
username?.startsWith("#deleted") == true
|
||||
|
||||
@@ -25,13 +33,15 @@ fun peerIsDeleted(
|
||||
currentUserId: Int? = null,
|
||||
deleted: Boolean? = null,
|
||||
username: String? = null,
|
||||
suspended: Boolean? = null,
|
||||
): Boolean {
|
||||
if (userId <= 0 || userId == currentUserId) return false
|
||||
if (deleted == true) return true
|
||||
if (isDeletedAccountUsername(username)) return true
|
||||
// Suspended peers look deleted to everyone except admin (id 1).
|
||||
if (suspended == true && currentUserId != 1) return true
|
||||
ProfileCache.get(userId)?.let { profile ->
|
||||
if (profile.isDeletedAccount(currentUserId)) return true
|
||||
if (isDeletedAccountUsername(profile.username)) return true
|
||||
if (profile.isRedactedPeerAccount(currentUserId)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -44,7 +54,7 @@ fun deletedUserDisplayNameForUi(): String =
|
||||
stringResource(Res.string.deleted_account)
|
||||
|
||||
suspend fun UserProfile.displayNameText(currentUserId: Int? = null): String {
|
||||
if (isDeletedAccount(currentUserId)) {
|
||||
if (isRedactedPeerAccount(currentUserId)) {
|
||||
return deletedUserDisplayName()
|
||||
}
|
||||
return visibleDisplayName(currentUserId).orEmpty()
|
||||
@@ -52,7 +62,7 @@ suspend fun UserProfile.displayNameText(currentUserId: Int? = null): String {
|
||||
|
||||
@Composable
|
||||
fun UserProfile.displayNameForUi(currentUserId: Int? = null): String =
|
||||
if (isDeletedAccount(currentUserId)) {
|
||||
if (isRedactedPeerAccount(currentUserId)) {
|
||||
deletedUserDisplayNameForUi()
|
||||
} else {
|
||||
visibleDisplayName(currentUserId).orEmpty()
|
||||
@@ -60,7 +70,7 @@ fun UserProfile.displayNameForUi(currentUserId: Int? = null): String =
|
||||
|
||||
/** Display name for avatar initials/gradient only; never falls back to username. */
|
||||
fun UserProfile.avatarLabelForInitials(currentUserId: Int? = null): String =
|
||||
if (isDeletedAccount(currentUserId)) {
|
||||
if (isRedactedPeerAccount(currentUserId)) {
|
||||
""
|
||||
} else {
|
||||
displayName?.trim().orEmpty()
|
||||
|
||||
Reference in New Issue
Block a user