feat: implement data and power settings sections
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
package ru.daemonlord.messenger.ui.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -22,14 +26,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.BatterySaver
|
||||
import androidx.compose.material.icons.filled.Chat
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material.icons.filled.Devices
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
@@ -58,7 +61,9 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -66,6 +71,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -77,6 +83,10 @@ import ru.daemonlord.messenger.domain.settings.model.AppLanguage
|
||||
import ru.daemonlord.messenger.domain.settings.model.AppThemeMode
|
||||
import ru.daemonlord.messenger.ui.account.AccountUiState
|
||||
import ru.daemonlord.messenger.ui.account.AccountViewModel
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private enum class SettingsFolder {
|
||||
Account,
|
||||
@@ -84,7 +94,6 @@ private enum class SettingsFolder {
|
||||
Privacy,
|
||||
Notifications,
|
||||
Data,
|
||||
Folders,
|
||||
Devices,
|
||||
Power,
|
||||
Language,
|
||||
@@ -200,15 +209,41 @@ private fun SettingsHome(
|
||||
|
||||
SettingsCard {
|
||||
SettingsRow(Icons.Filled.AccountCircle, stringResource(id = R.string.settings_folder_account), stringResource(id = R.string.settings_account_subtitle)) { onOpenFolder(SettingsFolder.Account) }
|
||||
SettingsRow(Icons.Filled.Chat, stringResource(id = R.string.settings_folder_chat), stringResource(id = R.string.settings_chat_subtitle)) { onOpenFolder(SettingsFolder.Chat) }
|
||||
SettingsRow(Icons.AutoMirrored.Filled.Chat, stringResource(id = R.string.settings_folder_chat), stringResource(id = R.string.settings_chat_subtitle)) { onOpenFolder(SettingsFolder.Chat) }
|
||||
SettingsRow(Icons.Filled.Lock, stringResource(id = R.string.settings_folder_privacy), stringResource(id = R.string.settings_privacy_subtitle)) { onOpenFolder(SettingsFolder.Privacy) }
|
||||
SettingsRow(Icons.Filled.Notifications, stringResource(id = R.string.settings_folder_notifications), stringResource(id = R.string.settings_notifications_subtitle)) { onOpenFolder(SettingsFolder.Notifications) }
|
||||
SettingsRow(Icons.Filled.Storage, stringResource(id = R.string.settings_folder_data), stringResource(id = R.string.settings_data_subtitle)) { onOpenFolder(SettingsFolder.Data) }
|
||||
SettingsRow(Icons.Filled.Folder, stringResource(id = R.string.settings_folder_folders), stringResource(id = R.string.settings_folders_subtitle)) { onOpenFolder(SettingsFolder.Folders) }
|
||||
SettingsRow(Icons.Filled.Devices, stringResource(id = R.string.settings_folder_devices), stringResource(id = R.string.settings_devices_subtitle)) { onOpenFolder(SettingsFolder.Devices) }
|
||||
SettingsRow(Icons.Filled.BatterySaver, stringResource(id = R.string.settings_folder_power), stringResource(id = R.string.settings_power_subtitle)) { onOpenFolder(SettingsFolder.Power) }
|
||||
SettingsRow(Icons.Filled.Language, stringResource(id = R.string.settings_folder_language), languageLabel(state.appLanguage), divider = false) { onOpenFolder(SettingsFolder.Language) }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
Text(
|
||||
text = stringResource(id = R.string.settings_preferences_header),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
SettingsToggle(
|
||||
icon = Icons.Filled.Notifications,
|
||||
title = stringResource(id = R.string.settings_enable_notifications),
|
||||
checked = state.notificationsEnabled,
|
||||
onCheckedChange = {},
|
||||
enabled = false,
|
||||
)
|
||||
SettingsToggle(
|
||||
icon = Icons.Filled.Visibility,
|
||||
title = stringResource(id = R.string.settings_show_preview),
|
||||
checked = state.notificationsPreviewEnabled,
|
||||
onCheckedChange = {},
|
||||
enabled = false,
|
||||
)
|
||||
SettingsShortcut(
|
||||
title = stringResource(id = R.string.settings_folder_language),
|
||||
subtitle = languageLabel(state.appLanguage),
|
||||
onClick = { onOpenFolder(SettingsFolder.Language) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,10 +282,9 @@ private fun SettingsFolderView(
|
||||
SettingsFolder.Chat -> ChatFolder(state, viewModel)
|
||||
SettingsFolder.Privacy -> PrivacyFolder(state, viewModel)
|
||||
SettingsFolder.Notifications -> NotificationsFolder(state, viewModel)
|
||||
SettingsFolder.Data -> DataFolder()
|
||||
SettingsFolder.Devices -> DevicesFolder(state, viewModel)
|
||||
SettingsFolder.Data -> PlaceholderFolder(stringResource(id = R.string.settings_placeholder_data))
|
||||
SettingsFolder.Folders -> PlaceholderFolder(stringResource(id = R.string.settings_placeholder_folders))
|
||||
SettingsFolder.Power -> PlaceholderFolder(stringResource(id = R.string.settings_placeholder_power))
|
||||
SettingsFolder.Power -> PowerFolder()
|
||||
SettingsFolder.Language -> LanguageFolder(state, viewModel)
|
||||
}
|
||||
|
||||
@@ -326,7 +360,6 @@ private fun folderTitle(folder: SettingsFolder): String = when (folder) {
|
||||
SettingsFolder.Privacy -> stringResource(id = R.string.settings_folder_privacy)
|
||||
SettingsFolder.Notifications -> stringResource(id = R.string.settings_folder_notifications)
|
||||
SettingsFolder.Data -> stringResource(id = R.string.settings_folder_data)
|
||||
SettingsFolder.Folders -> stringResource(id = R.string.settings_folder_folders)
|
||||
SettingsFolder.Devices -> stringResource(id = R.string.settings_folder_devices)
|
||||
SettingsFolder.Power -> stringResource(id = R.string.settings_folder_power)
|
||||
SettingsFolder.Language -> stringResource(id = R.string.settings_folder_language)
|
||||
@@ -517,8 +550,231 @@ private fun DevicesFolder(state: AccountUiState, viewModel: AccountViewModel) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlaceholderFolder(text: String) {
|
||||
SettingsCard { Text(text) }
|
||||
private fun DataFolder() {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var cacheStats by remember { mutableStateOf<SettingsCacheStats?>(null) }
|
||||
var isClearing by remember { mutableStateOf(false) }
|
||||
|
||||
suspend fun refreshStats() {
|
||||
cacheStats = readSettingsCacheStats(context)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
refreshStats()
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
Text(stringResource(id = R.string.settings_data_usage_title), style = MaterialTheme.typography.titleSmall)
|
||||
val stats = cacheStats
|
||||
if (stats == null) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
SettingsMetricRow(
|
||||
title = stringResource(id = R.string.settings_data_images_cache),
|
||||
value = formatSettingsBytes(stats.coilImagesBytes),
|
||||
)
|
||||
SettingsMetricRow(
|
||||
title = stringResource(id = R.string.settings_data_media_cache),
|
||||
value = formatSettingsBytes(stats.exoMediaBytes),
|
||||
)
|
||||
SettingsMetricRow(
|
||||
title = stringResource(id = R.string.settings_data_temp_captures),
|
||||
value = formatSettingsBytes(stats.capturesBytes),
|
||||
)
|
||||
SettingsMetricRow(
|
||||
title = stringResource(id = R.string.settings_data_total_cache),
|
||||
value = formatSettingsBytes(stats.totalBytes),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
Text(stringResource(id = R.string.settings_data_actions_title), style = MaterialTheme.typography.titleSmall)
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
isClearing = true
|
||||
withContext(Dispatchers.IO) {
|
||||
clearSettingsCacheDirectories(context)
|
||||
}
|
||||
refreshStats()
|
||||
isClearing = false
|
||||
}
|
||||
},
|
||||
enabled = !isClearing,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
if (isClearing) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
Icon(Icons.Filled.DeleteOutline, contentDescription = null)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = R.string.settings_data_clear_cache),
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = R.string.settings_data_clear_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PowerFolder() {
|
||||
val context = LocalContext.current
|
||||
val powerManager = remember {
|
||||
context.getSystemService(PowerManager::class.java)
|
||||
}
|
||||
val powerSnapshot = produceState(initialValue = readPowerState(context, powerManager)) {
|
||||
value = readPowerState(context, powerManager)
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
Text(stringResource(id = R.string.settings_power_status_title), style = MaterialTheme.typography.titleSmall)
|
||||
SettingsMetricRow(
|
||||
title = stringResource(id = R.string.settings_power_saver_enabled),
|
||||
value = if (powerSnapshot.value.isPowerSaveMode) {
|
||||
context.getString(R.string.settings_status_enabled)
|
||||
} else {
|
||||
context.getString(R.string.settings_status_disabled)
|
||||
},
|
||||
)
|
||||
SettingsMetricRow(
|
||||
title = stringResource(id = R.string.settings_power_optimizations),
|
||||
value = if (powerSnapshot.value.ignoresOptimizations) {
|
||||
context.getString(R.string.settings_power_not_optimized)
|
||||
} else {
|
||||
context.getString(R.string.settings_power_optimized)
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.settings_power_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
Text(stringResource(id = R.string.settings_power_actions_title), style = MaterialTheme.typography.titleSmall)
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
context.startActivity(Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(Icons.Filled.BatterySaver, contentDescription = null)
|
||||
Text(
|
||||
text = stringResource(id = R.string.settings_power_open_battery_saver),
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
|
||||
} else {
|
||||
Settings.ACTION_SETTINGS
|
||||
}
|
||||
context.startActivity(Intent(action).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(Icons.Filled.Security, contentDescription = null)
|
||||
Text(
|
||||
text = stringResource(id = R.string.settings_power_open_optimization_settings),
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class SettingsCacheStats(
|
||||
val coilImagesBytes: Long,
|
||||
val exoMediaBytes: Long,
|
||||
val capturesBytes: Long,
|
||||
) {
|
||||
val totalBytes: Long = coilImagesBytes + exoMediaBytes + capturesBytes
|
||||
}
|
||||
|
||||
private data class PowerSnapshot(
|
||||
val isPowerSaveMode: Boolean,
|
||||
val ignoresOptimizations: Boolean,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun SettingsMetricRow(title: String, value: String) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun readSettingsCacheStats(context: android.content.Context): SettingsCacheStats = withContext(Dispatchers.IO) {
|
||||
SettingsCacheStats(
|
||||
coilImagesBytes = File(context.cacheDir, "coil_images").directorySize(),
|
||||
exoMediaBytes = File(context.cacheDir, "exo_media_cache").directorySize(),
|
||||
capturesBytes = File(context.cacheDir, "captures").directorySize(),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun clearSettingsCacheDirectories(context: android.content.Context) = withContext(Dispatchers.IO) {
|
||||
listOf(
|
||||
File(context.cacheDir, "coil_images"),
|
||||
File(context.cacheDir, "exo_media_cache"),
|
||||
File(context.cacheDir, "captures"),
|
||||
).forEach { file ->
|
||||
if (file.exists()) {
|
||||
file.deleteRecursively()
|
||||
file.mkdirs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun File.directorySize(): Long {
|
||||
if (!exists()) return 0L
|
||||
if (isFile) return length()
|
||||
return walkTopDown()
|
||||
.filter { it.isFile }
|
||||
.sumOf { it.length() }
|
||||
}
|
||||
|
||||
private fun formatSettingsBytes(value: Long): String {
|
||||
if (value < 1024) return "$value B"
|
||||
val kb = value / 1024.0
|
||||
if (kb < 1024) return String.format("%.1f KB", kb)
|
||||
val mb = kb / 1024.0
|
||||
if (mb < 1024) return String.format("%.1f MB", mb)
|
||||
val gb = mb / 1024.0
|
||||
return String.format("%.1f GB", gb)
|
||||
}
|
||||
|
||||
private fun readPowerState(context: android.content.Context, powerManager: PowerManager?): PowerSnapshot {
|
||||
val isSaver = powerManager?.isPowerSaveMode == true
|
||||
val ignores = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
powerManager?.isIgnoringBatteryOptimizations(context.packageName) == true
|
||||
} else {
|
||||
true
|
||||
}
|
||||
return PowerSnapshot(
|
||||
isPowerSaveMode = isSaver,
|
||||
ignoresOptimizations = ignores,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -600,7 +856,13 @@ private fun SettingsRow(icon: ImageVector, title: String, subtitle: String, divi
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsToggle(icon: ImageVector, title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
private fun SettingsToggle(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(14.dp)).background(MaterialTheme.colorScheme.surfaceContainerHighest).padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -608,7 +870,7 @@ private fun SettingsToggle(icon: ImageVector, title: String, checked: Boolean, o
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
|
||||
Text(title, modifier = Modifier.weight(1f))
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange, enabled = enabled)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -260,6 +260,26 @@
|
||||
<string name="settings_folders_subtitle">Сортировка чатов по папкам</string>
|
||||
<string name="settings_devices_subtitle">Управление активными сессиями</string>
|
||||
<string name="settings_power_subtitle">Экономия энергии при низком заряде</string>
|
||||
<string name="settings_preferences_header">Быстрые настройки</string>
|
||||
<string name="settings_data_usage_title">Кэшированные медиа</string>
|
||||
<string name="settings_data_images_cache">Кэш изображений</string>
|
||||
<string name="settings_data_media_cache">Кэш видео и аудио</string>
|
||||
<string name="settings_data_temp_captures">Временные записи</string>
|
||||
<string name="settings_data_total_cache">Всего кэша</string>
|
||||
<string name="settings_data_actions_title">Действия с данными</string>
|
||||
<string name="settings_data_clear_cache">Очистить кэш медиа</string>
|
||||
<string name="settings_data_clear_hint">Это удалит временные копии медиа и освободит место. Сами чаты и файлы на сервере не пострадают.</string>
|
||||
<string name="settings_power_status_title">Состояние энергосбережения</string>
|
||||
<string name="settings_power_saver_enabled">Режим энергосбережения</string>
|
||||
<string name="settings_power_optimizations">Оптимизация батареи</string>
|
||||
<string name="settings_power_not_optimized">Без ограничений</string>
|
||||
<string name="settings_power_optimized">Оптимизируется системой</string>
|
||||
<string name="settings_power_hint">Используйте системные настройки, если уведомления или синхронизация в фоне становятся слишком агрессивно ограничены.</string>
|
||||
<string name="settings_power_actions_title">Действия</string>
|
||||
<string name="settings_power_open_battery_saver">Открыть настройки энергосбережения</string>
|
||||
<string name="settings_power_open_optimization_settings">Открыть настройки оптимизации</string>
|
||||
<string name="settings_status_enabled">Включено</string>
|
||||
<string name="settings_status_disabled">Выключено</string>
|
||||
<string name="settings_placeholder_data">Этот раздел будет расширен на следующем шаге.</string>
|
||||
<string name="settings_placeholder_folders">Управление папками чатов будет добавлено на следующей итерации.</string>
|
||||
<string name="settings_placeholder_power">Настройки энергосбережения будут добавлены отдельным этапом.</string>
|
||||
|
||||
@@ -260,6 +260,26 @@
|
||||
<string name="settings_folders_subtitle">Sort chats by folders</string>
|
||||
<string name="settings_devices_subtitle">Manage active sessions</string>
|
||||
<string name="settings_power_subtitle">Save power on low battery</string>
|
||||
<string name="settings_preferences_header">Quick preferences</string>
|
||||
<string name="settings_data_usage_title">Cached media</string>
|
||||
<string name="settings_data_images_cache">Image cache</string>
|
||||
<string name="settings_data_media_cache">Video and audio cache</string>
|
||||
<string name="settings_data_temp_captures">Temporary captures</string>
|
||||
<string name="settings_data_total_cache">Total cached data</string>
|
||||
<string name="settings_data_actions_title">Data actions</string>
|
||||
<string name="settings_data_clear_cache">Clear cached media</string>
|
||||
<string name="settings_data_clear_hint">This clears temporary media copies and frees local space. Chats and server files stay intact.</string>
|
||||
<string name="settings_power_status_title">Power status</string>
|
||||
<string name="settings_power_saver_enabled">Battery saver</string>
|
||||
<string name="settings_power_optimizations">Battery optimization</string>
|
||||
<string name="settings_power_not_optimized">Not restricted</string>
|
||||
<string name="settings_power_optimized">Optimized by the system</string>
|
||||
<string name="settings_power_hint">Use system settings if message delivery or background sync become aggressive under battery restrictions.</string>
|
||||
<string name="settings_power_actions_title">Power actions</string>
|
||||
<string name="settings_power_open_battery_saver">Open battery saver settings</string>
|
||||
<string name="settings_power_open_optimization_settings">Open optimization settings</string>
|
||||
<string name="settings_status_enabled">Enabled</string>
|
||||
<string name="settings_status_disabled">Disabled</string>
|
||||
<string name="settings_placeholder_data">This section will be expanded in the next step.</string>
|
||||
<string name="settings_placeholder_folders">Chat folders management will be added in next iteration.</string>
|
||||
<string name="settings_placeholder_power">Power saving settings will be added in a separate step.</string>
|
||||
|
||||
Reference in New Issue
Block a user