QR экран: обои вместо градиентов, circular reveal анимация при смене обоев и темы, кнопка sun/moon переключает тему приложения, cooldown 600ms. Убрана кнопка Start New Call с экрана звонков.
This commit is contained in:
@@ -1863,7 +1863,8 @@ fun MainScreen(
|
||||
onScanQr = {
|
||||
navStack = navStack.filterNot { it is Screen.MyQr }
|
||||
pushScreen(Screen.QrScanner)
|
||||
}
|
||||
},
|
||||
onToggleTheme = onToggleTheme
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -109,11 +109,6 @@ fun CallsHistoryScreen(
|
||||
contentPadding = PaddingValues(bottom = 16.dp)
|
||||
) {
|
||||
item(key = "start_new_call") {
|
||||
StartNewCallRow(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onClick = onStartNewCall
|
||||
)
|
||||
Divider(color = dividerColor, thickness = 0.5.dp)
|
||||
Text(
|
||||
text = "You can add up to 200 participants to a call.",
|
||||
color = secondaryTextColor,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.rosetta.messenger.ui.qr
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -10,55 +13,69 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInRoot
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.view.drawToBitmap
|
||||
import com.rosetta.messenger.R
|
||||
import com.rosetta.messenger.repository.AvatarRepository
|
||||
import com.rosetta.messenger.ui.components.AvatarImage
|
||||
import com.rosetta.messenger.ui.settings.ThemeWallpapers
|
||||
import com.rosetta.messenger.utils.QrCodeGenerator
|
||||
import compose.icons.TablerIcons
|
||||
import compose.icons.tablericons.*
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.hypot
|
||||
|
||||
// Theme presets — gradient background + QR foreground color
|
||||
// QR theme = wallpaper + QR color
|
||||
private data class QrTheme(
|
||||
val gradientColors: List<Color>,
|
||||
val wallpaperId: String,
|
||||
val qrColor: Int,
|
||||
val name: String
|
||||
val tintColor: Color,
|
||||
val isDark: Boolean
|
||||
)
|
||||
|
||||
private val qrThemes = listOf(
|
||||
QrTheme(
|
||||
listOf(Color(0xFF667EEA), Color(0xFF764BA2)),
|
||||
0xFF4A3F9F.toInt(),
|
||||
"Purple"
|
||||
),
|
||||
QrTheme(
|
||||
listOf(Color(0xFF43E97B), Color(0xFF38F9D7)),
|
||||
0xFF2D8F5E.toInt(),
|
||||
"Green"
|
||||
),
|
||||
QrTheme(
|
||||
listOf(Color(0xFFF78CA0), Color(0xFFF9748F), Color(0xFFFD868C)),
|
||||
0xFFBF3A5A.toInt(),
|
||||
"Pink"
|
||||
),
|
||||
QrTheme(
|
||||
listOf(Color(0xFF4FACFE), Color(0xFF00F2FE)),
|
||||
0xFF2171B5.toInt(),
|
||||
"Blue"
|
||||
),
|
||||
QrTheme(
|
||||
listOf(Color(0xFFFFA751), Color(0xFFFFE259)),
|
||||
0xFFC67B1C.toInt(),
|
||||
"Orange"
|
||||
),
|
||||
QrTheme("dark_01", 0xFF5AA5FF.toInt(), Color(0xFF5AA5FF), true),
|
||||
QrTheme("dark_02", 0xFF9575CD.toInt(), Color(0xFF9575CD), true),
|
||||
QrTheme("dark_03", 0xFF4DB6AC.toInt(), Color(0xFF4DB6AC), true),
|
||||
QrTheme("light_01", 0xFF228BE6.toInt(), Color(0xFF228BE6), false),
|
||||
QrTheme("light_02", 0xFF43A047.toInt(), Color(0xFF43A047), false),
|
||||
QrTheme("light_03", 0xFFE64980.toInt(), Color(0xFFE64980), false),
|
||||
)
|
||||
|
||||
private fun maxRevealRadius(center: Offset, bounds: IntSize): Float {
|
||||
if (bounds.width <= 0 || bounds.height <= 0) return 0f
|
||||
val w = bounds.width.toFloat()
|
||||
val h = bounds.height.toFloat()
|
||||
return maxOf(
|
||||
hypot(center.x, center.y),
|
||||
hypot(w - center.x, center.y),
|
||||
hypot(center.x, h - center.y),
|
||||
hypot(w - center.x, h - center.y)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MyQrCodeScreen(
|
||||
isDarkTheme: Boolean,
|
||||
@@ -67,12 +84,31 @@ fun MyQrCodeScreen(
|
||||
username: String,
|
||||
avatarRepository: AvatarRepository? = null,
|
||||
onBack: () -> Unit,
|
||||
onScanQr: () -> Unit = {}
|
||||
onScanQr: () -> Unit = {},
|
||||
onToggleTheme: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var selectedThemeIndex by remember { mutableIntStateOf(0) }
|
||||
val view = LocalView.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var selectedThemeIndex by remember { mutableIntStateOf(if (isDarkTheme) 0 else 3) }
|
||||
|
||||
// Auto-switch to matching theme group when app theme changes
|
||||
LaunchedEffect(isDarkTheme) {
|
||||
val currentTheme = qrThemes.getOrNull(selectedThemeIndex)
|
||||
if (currentTheme != null && currentTheme.isDark != isDarkTheme) {
|
||||
// Map to same position in the other group
|
||||
val posInGroup = if (currentTheme.isDark) selectedThemeIndex else selectedThemeIndex - 3
|
||||
selectedThemeIndex = if (isDarkTheme) posInGroup.coerceIn(0, 2) else (posInGroup + 3).coerceIn(3, 5)
|
||||
}
|
||||
}
|
||||
|
||||
val theme = qrThemes[selectedThemeIndex]
|
||||
|
||||
val wallpaperResId = remember(theme.wallpaperId) {
|
||||
ThemeWallpapers.drawableResOrNull(theme.wallpaperId)
|
||||
}
|
||||
|
||||
val qrBitmap = remember(publicKey, selectedThemeIndex) {
|
||||
QrCodeGenerator.generateQrBitmap(
|
||||
content = QrCodeGenerator.profilePayload(publicKey),
|
||||
@@ -88,27 +124,90 @@ fun MyQrCodeScreen(
|
||||
else QrCodeGenerator.profileShareUrlByKey(publicKey)
|
||||
}
|
||||
|
||||
// Circular reveal state
|
||||
val revealRadius = remember { Animatable(0f) }
|
||||
var revealActive by remember { mutableStateOf(false) }
|
||||
var revealSnapshot by remember { mutableStateOf<ImageBitmap?>(null) }
|
||||
var revealCenter by remember { mutableStateOf(Offset.Zero) }
|
||||
var rootSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
var lastRevealTime by remember { mutableLongStateOf(0L) }
|
||||
val revealCooldownMs = 600L
|
||||
|
||||
fun startReveal(newIndex: Int, center: Offset) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (revealActive || newIndex == selectedThemeIndex || now - lastRevealTime < revealCooldownMs) return
|
||||
lastRevealTime = now
|
||||
if (rootSize.width <= 0 || rootSize.height <= 0) {
|
||||
selectedThemeIndex = newIndex
|
||||
return
|
||||
}
|
||||
|
||||
val snapshot = runCatching { view.drawToBitmap() }.getOrNull()
|
||||
if (snapshot == null) {
|
||||
selectedThemeIndex = newIndex
|
||||
return
|
||||
}
|
||||
|
||||
val maxR = maxRevealRadius(center, rootSize)
|
||||
if (maxR <= 0f) {
|
||||
selectedThemeIndex = newIndex
|
||||
return
|
||||
}
|
||||
|
||||
revealActive = true
|
||||
revealCenter = center
|
||||
revealSnapshot = snapshot.asImageBitmap()
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
revealRadius.snapTo(0f)
|
||||
selectedThemeIndex = newIndex
|
||||
kotlinx.coroutines.delay(16) // wait one frame for recomposition
|
||||
revealRadius.animateTo(
|
||||
targetValue = maxR,
|
||||
animationSpec = tween(
|
||||
durationMillis = 400,
|
||||
easing = CubicBezierEasing(0.45f, 0.05f, 0.55f, 0.95f)
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
revealSnapshot = null
|
||||
revealActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Brush.verticalGradient(theme.gradientColors))
|
||||
.onSizeChanged { rootSize = it }
|
||||
) {
|
||||
// Wallpaper background
|
||||
if (wallpaperResId != null) {
|
||||
Image(
|
||||
painter = painterResource(id = wallpaperResId),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize().background(if (theme.isDark) Color(0xFF1A1A1A) else Color(0xFFF0F0F5)))
|
||||
}
|
||||
|
||||
// Main content
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.statusBarsPadding().height(40.dp))
|
||||
|
||||
// QR Card with avatar overlapping
|
||||
// QR Card with overlapping avatar
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
// White card
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 40.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 40.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
@@ -119,7 +218,6 @@ fun MyQrCodeScreen(
|
||||
.padding(top = 52.dp, bottom = 24.dp, start = 24.dp, end = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// QR code
|
||||
if (qrBitmap != null) {
|
||||
Image(
|
||||
bitmap = qrBitmap.asImageBitmap(),
|
||||
@@ -130,7 +228,6 @@ fun MyQrCodeScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Username
|
||||
Text(
|
||||
text = if (username.isNotBlank()) "@${username.uppercase()}"
|
||||
else displayName.uppercase(),
|
||||
@@ -142,7 +239,7 @@ fun MyQrCodeScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar overlapping the card top
|
||||
// Avatar overlapping
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
@@ -163,12 +260,11 @@ fun MyQrCodeScreen(
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Bottom sheet area
|
||||
// Bottom sheet
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
|
||||
color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 16.dp
|
||||
) {
|
||||
Column(
|
||||
@@ -178,71 +274,120 @@ fun MyQrCodeScreen(
|
||||
.padding(top = 16.dp, bottom = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Handle bar
|
||||
// Handle
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(36.dp)
|
||||
.height(4.dp)
|
||||
.width(36.dp).height(4.dp)
|
||||
.clip(RoundedCornerShape(2.dp))
|
||||
.background(Color.Gray.copy(alpha = 0.3f))
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Header: Close + title
|
||||
// Header: X + "QR Code" + theme toggle
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(TablerIcons.X, contentDescription = "Close",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("QR Code", fontSize = 17.sp, fontWeight = FontWeight.SemiBold,
|
||||
color = if (isDarkTheme) Color.White else Color.Black)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
var themeButtonPos by remember { mutableStateOf(Offset.Zero) }
|
||||
IconButton(
|
||||
onClick = {
|
||||
// Snapshot → toggle theme → circular reveal
|
||||
val now = System.currentTimeMillis()
|
||||
if (!revealActive && rootSize.width > 0 && now - lastRevealTime >= revealCooldownMs) {
|
||||
lastRevealTime = now
|
||||
val snapshot = runCatching { view.drawToBitmap() }.getOrNull()
|
||||
if (snapshot != null) {
|
||||
val maxR = maxRevealRadius(themeButtonPos, rootSize)
|
||||
revealActive = true
|
||||
revealCenter = themeButtonPos
|
||||
revealSnapshot = snapshot.asImageBitmap()
|
||||
// Switch to matching wallpaper in new theme
|
||||
val posInGroup = if (isDarkTheme) selectedThemeIndex else selectedThemeIndex - 3
|
||||
val newIndex = if (isDarkTheme) (posInGroup + 3).coerceIn(3, 5) else posInGroup.coerceIn(0, 2)
|
||||
selectedThemeIndex = newIndex
|
||||
onToggleTheme()
|
||||
scope.launch {
|
||||
try {
|
||||
revealRadius.snapTo(0f)
|
||||
kotlinx.coroutines.delay(16)
|
||||
revealRadius.animateTo(
|
||||
targetValue = maxR,
|
||||
animationSpec = tween(400, easing = CubicBezierEasing(0.45f, 0.05f, 0.55f, 0.95f))
|
||||
)
|
||||
} finally {
|
||||
revealSnapshot = null
|
||||
revealActive = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onToggleTheme()
|
||||
}
|
||||
} else {
|
||||
onToggleTheme()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.onGloballyPositioned { coords ->
|
||||
val pos = coords.positionInRoot()
|
||||
val sz = coords.size
|
||||
themeButtonPos = Offset(pos.x + sz.width / 2f, pos.y + sz.height / 2f)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
TablerIcons.X,
|
||||
contentDescription = "Close",
|
||||
imageVector = if (isDarkTheme) TablerIcons.Sun else TablerIcons.MoonStars,
|
||||
contentDescription = "Toggle theme",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
"QR Code",
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = if (isDarkTheme) Color.White else Color.Black
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.width(48.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Theme selector
|
||||
// Wallpaper selector — show current theme's wallpapers
|
||||
val currentThemes = qrThemes.filter { it.isDark == isDarkTheme }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
qrThemes.forEachIndexed { index, t ->
|
||||
currentThemes.forEach { t ->
|
||||
val index = qrThemes.indexOf(t)
|
||||
val isSelected = index == selectedThemeIndex
|
||||
val wpRes = ThemeWallpapers.drawableResOrNull(t.wallpaperId)
|
||||
var itemPosition by remember { mutableStateOf(Offset.Zero) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.aspectRatio(0.85f)
|
||||
.weight(1f).aspectRatio(0.85f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.border(
|
||||
width = if (isSelected) 2.dp else 0.dp,
|
||||
color = if (isSelected) Color(0xFF3390EC) else Color.Transparent,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.background(Brush.verticalGradient(t.gradientColors))
|
||||
.clickable { selectedThemeIndex = index },
|
||||
.onGloballyPositioned { coords ->
|
||||
val pos = coords.positionInRoot()
|
||||
val sz = coords.size
|
||||
itemPosition = Offset(pos.x + sz.width / 2f, pos.y + sz.height / 2f)
|
||||
}
|
||||
.clickable { if (index != selectedThemeIndex) startReveal(index, itemPosition) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
TablerIcons.Scan,
|
||||
contentDescription = t.name,
|
||||
tint = Color.White.copy(alpha = 0.8f),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
if (wpRes != null) {
|
||||
Image(painter = painterResource(id = wpRes), contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop)
|
||||
}
|
||||
Icon(TablerIcons.Scan, contentDescription = null,
|
||||
tint = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.35f),
|
||||
modifier = Modifier.size(22.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,10 +403,7 @@ fun MyQrCodeScreen(
|
||||
}
|
||||
context.startActivity(Intent.createChooser(intent, "Share Profile"))
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(50.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).height(50.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF3390EC)),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
@@ -270,24 +412,45 @@ fun MyQrCodeScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Scan QR button
|
||||
// Scan QR
|
||||
TextButton(onClick = onScanQr) {
|
||||
Icon(
|
||||
TablerIcons.Scan,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF3390EC),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Icon(TablerIcons.Scan, contentDescription = null,
|
||||
tint = Color(0xFF3390EC), modifier = Modifier.size(20.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
"Scan QR Code",
|
||||
color = Color(0xFF3390EC),
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text("Scan QR Code", color = Color(0xFF3390EC),
|
||||
fontSize = 15.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Circular reveal overlay
|
||||
if (revealActive) {
|
||||
val snapshot = revealSnapshot
|
||||
if (snapshot != null) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||
) {
|
||||
val dstSize = IntSize(size.width.toInt(), size.height.toInt())
|
||||
// Draw old screenshot
|
||||
drawImage(
|
||||
image = snapshot,
|
||||
srcOffset = IntOffset.Zero,
|
||||
srcSize = IntSize(snapshot.width, snapshot.height),
|
||||
dstOffset = IntOffset.Zero,
|
||||
dstSize = dstSize
|
||||
)
|
||||
// Cut expanding circle to reveal new theme underneath
|
||||
drawCircle(
|
||||
color = Color.Transparent,
|
||||
radius = revealRadius.value,
|
||||
center = revealCenter,
|
||||
blendMode = BlendMode.Clear
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user