Improves the speed of Location services.
This commit is contained in:
@@ -27,6 +27,7 @@ import android.location.Location
|
|||||||
import android.location.LocationListener
|
import android.location.LocationListener
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
|
import android.util.LruCache
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -92,7 +93,33 @@ class LocationUtil(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReverseGeoLocationUtil {
|
object CachedGeoLocations {
|
||||||
|
val locationNames = LruCache<String, String>(20)
|
||||||
|
|
||||||
|
fun cached(geoHashStr: String): String? {
|
||||||
|
return locationNames[geoHashStr]
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun geoLocate(
|
||||||
|
geoHashStr: String,
|
||||||
|
location: Location,
|
||||||
|
context: Context,
|
||||||
|
): String? {
|
||||||
|
locationNames[geoHashStr]?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
val name = ReverseGeoLocationUtil().execute(location, context)?.ifBlank { null }
|
||||||
|
|
||||||
|
if (name != null) {
|
||||||
|
locationNames.put(geoHashStr, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ReverseGeoLocationUtil {
|
||||||
suspend fun execute(
|
suspend fun execute(
|
||||||
location: Location,
|
location: Location,
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ import androidx.compose.ui.window.DialogProperties
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.fonfon.kgeohash.toGeoHash
|
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
@@ -134,7 +133,6 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
import com.vitorpamplona.amethyst.service.Nip96MediaServers
|
||||||
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
|
||||||
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
|
||||||
import com.vitorpamplona.amethyst.ui.components.BechLink
|
import com.vitorpamplona.amethyst.ui.components.BechLink
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||||
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
|
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
|
||||||
@@ -144,6 +142,7 @@ import com.vitorpamplona.amethyst.ui.components.ZapRaiserRequest
|
|||||||
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.CancelIcon
|
import com.vitorpamplona.amethyst.ui.note.CancelIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.CloseIcon
|
import com.vitorpamplona.amethyst.ui.note.CloseIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.LoadCityName
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||||
import com.vitorpamplona.amethyst.ui.note.PollIcon
|
import com.vitorpamplona.amethyst.ui.note.PollIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.RegularPostIcon
|
import com.vitorpamplona.amethyst.ui.note.RegularPostIcon
|
||||||
@@ -1172,23 +1171,12 @@ fun FowardZapTo(
|
|||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LocationAsHash(postViewModel: NewPostViewModel) {
|
fun LocationAsHash(postViewModel: NewPostViewModel) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val locationPermissionState =
|
val locationPermissionState =
|
||||||
rememberPermissionState(
|
rememberPermissionState(
|
||||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (locationPermissionState.status.isGranted) {
|
if (locationPermissionState.status.isGranted) {
|
||||||
var locationDescriptionFlow by remember(postViewModel) { mutableStateOf<Flow<String>?>(null) }
|
|
||||||
|
|
||||||
DisposableEffect(key1 = Unit) {
|
|
||||||
postViewModel.startLocation(context = context)
|
|
||||||
locationDescriptionFlow = postViewModel.location
|
|
||||||
|
|
||||||
onDispose { postViewModel.stopLocation() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
@@ -1219,7 +1207,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
|
|||||||
modifier = Modifier.padding(start = 10.dp),
|
modifier = Modifier.padding(start = 10.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
locationDescriptionFlow?.let { geoLocation -> DisplayLocationObserver(geoLocation) }
|
DisplayLocationObserver(postViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
@@ -1236,41 +1224,39 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayLocationObserver(geoLocation: Flow<String>) {
|
fun DisplayLocationObserver(postViewModel: NewPostViewModel) {
|
||||||
val location by geoLocation.collectAsStateWithLifecycle(null)
|
val context = LocalContext.current
|
||||||
|
var locationDescriptionFlow by remember(postViewModel) { mutableStateOf<Flow<String>?>(null) }
|
||||||
|
|
||||||
location?.let { DisplayLocationInTitle(geohash = it) }
|
DisposableEffect(key1 = context) {
|
||||||
|
postViewModel.startLocation(context = context)
|
||||||
|
locationDescriptionFlow = postViewModel.location
|
||||||
|
|
||||||
|
onDispose { postViewModel.stopLocation() }
|
||||||
|
}
|
||||||
|
|
||||||
|
locationDescriptionFlow?.let {
|
||||||
|
val location by it.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
|
location?.let { DisplayLocationInTitle(geohash = it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayLocationInTitle(geohash: String) {
|
fun DisplayLocationInTitle(geohash: String) {
|
||||||
val context = LocalContext.current
|
LoadCityName(
|
||||||
|
geohashStr = geohash,
|
||||||
var cityName by remember(geohash) { mutableStateOf<String>(geohash) }
|
onLoading = {
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
LaunchedEffect(key1 = geohash) {
|
LoadingAnimation()
|
||||||
launch(Dispatchers.IO) {
|
},
|
||||||
val newCityName =
|
) { cityName ->
|
||||||
ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)?.ifBlank {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newCityName != null && newCityName != cityName) {
|
|
||||||
cityName = newCityName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (geohash != "s0000") {
|
|
||||||
Text(
|
Text(
|
||||||
text = cityName,
|
text = cityName,
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
fontWeight = FontWeight.W500,
|
fontWeight = FontWeight.W500,
|
||||||
modifier = Modifier.padding(start = Size5dp),
|
modifier = Modifier.padding(start = Size5dp),
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
Spacer(modifier = StdHorzSpacer)
|
|
||||||
LoadingAnimation()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ import androidx.lifecycle.map
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import com.fonfon.kgeohash.toGeoHash
|
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
@@ -817,15 +816,12 @@ fun SimpleTextSpinner(
|
|||||||
fun RenderOption(option: Name) {
|
fun RenderOption(option: Name) {
|
||||||
when (option) {
|
when (option) {
|
||||||
is GeoHashName -> {
|
is GeoHashName -> {
|
||||||
val geohash = runCatching { option.geoHashTag.toGeoHash() }.getOrNull()
|
LoadCityName(option.geoHashTag) {
|
||||||
if (geohash != null) {
|
Row(
|
||||||
LoadCityName(geohash) {
|
horizontalArrangement = Arrangement.Center,
|
||||||
Row(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center,
|
) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Text(text = "/g/$it", color = MaterialTheme.colorScheme.onSurface)
|
||||||
) {
|
|
||||||
Text(text = "/g/$it", color = MaterialTheme.colorScheme.onSurface)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.navigation
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -32,7 +31,6 @@ import androidx.navigation.NavBackStackEntry
|
|||||||
import androidx.navigation.NavDestination
|
import androidx.navigation.NavDestination
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.NavType
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ import coil.compose.AsyncImagePainter
|
|||||||
import coil.request.SuccessResult
|
import coil.request.SuccessResult
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.fonfon.kgeohash.GeoHash
|
|
||||||
import com.fonfon.kgeohash.toGeoHash
|
import com.fonfon.kgeohash.toGeoHash
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.BaseMediaContent
|
import com.vitorpamplona.amethyst.commons.BaseMediaContent
|
||||||
@@ -115,7 +114,7 @@ import com.vitorpamplona.amethyst.model.Channel
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
|
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
import com.vitorpamplona.amethyst.service.CachedGeoLocations
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
|
||||||
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
|
||||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||||
@@ -2768,23 +2767,34 @@ fun LoadOts(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadCityName(
|
fun LoadCityName(
|
||||||
geohash: GeoHash,
|
geohashStr: String,
|
||||||
|
onLoading: (@Composable () -> Unit)? = null,
|
||||||
content: @Composable (String) -> Unit,
|
content: @Composable (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
var cityName by remember(geohashStr) { mutableStateOf(CachedGeoLocations.cached(geohashStr)) }
|
||||||
var cityName by remember(geohash) { mutableStateOf<String>(geohash.toString()) }
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = geohash) {
|
if (cityName == null) {
|
||||||
launch(Dispatchers.IO) {
|
if (onLoading != null) {
|
||||||
val newCityName =
|
onLoading()
|
||||||
ReverseGeoLocationUtil().execute(geohash.toLocation(), context)?.ifBlank { null }
|
}
|
||||||
if (newCityName != null && newCityName != cityName) {
|
|
||||||
cityName = newCityName
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = geohashStr, context) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val geoHash = runCatching { geohashStr.toGeoHash() }.getOrNull()
|
||||||
|
if (geoHash != null) {
|
||||||
|
val newCityName =
|
||||||
|
CachedGeoLocations.geoLocate(geohashStr, geoHash.toLocation(), context)?.ifBlank { null }
|
||||||
|
if (newCityName != null && newCityName != cityName) {
|
||||||
|
cityName = newCityName
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
cityName?.let { content(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
content(cityName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -2792,24 +2802,21 @@ fun DisplayLocation(
|
|||||||
geohashStr: String,
|
geohashStr: String,
|
||||||
nav: (String) -> Unit,
|
nav: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val geoHash = runCatching { geohashStr.toGeoHash() }.getOrNull()
|
LoadCityName(geohashStr) { cityName ->
|
||||||
if (geoHash != null) {
|
ClickableText(
|
||||||
LoadCityName(geoHash) { cityName ->
|
text = AnnotatedString(cityName),
|
||||||
ClickableText(
|
onClick = { nav("Geohash/$geohashStr") },
|
||||||
text = AnnotatedString(cityName),
|
style =
|
||||||
onClick = { nav("Geohash/$geoHash") },
|
LocalTextStyle.current.copy(
|
||||||
style =
|
color =
|
||||||
LocalTextStyle.current.copy(
|
MaterialTheme.colorScheme.primary.copy(
|
||||||
color =
|
alpha = 0.52f,
|
||||||
MaterialTheme.colorScheme.primary.copy(
|
),
|
||||||
alpha = 0.52f,
|
fontSize = Font14SP,
|
||||||
),
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = Font14SP,
|
),
|
||||||
fontWeight = FontWeight.Bold,
|
maxLines = 1,
|
||||||
),
|
)
|
||||||
maxLines = 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,32 +31,26 @@ import androidx.compose.material3.Divider
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.fonfon.kgeohash.toGeoHash
|
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.NostrGeohashDataSource
|
import com.vitorpamplona.amethyst.service.NostrGeohashDataSource
|
||||||
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
|
import com.vitorpamplona.amethyst.ui.note.LoadCityName
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrGeoHashFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrGeoHashFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GeoHashScreen(
|
fun GeoHashScreen(
|
||||||
@@ -174,27 +168,13 @@ fun DislayGeoTagHeader(
|
|||||||
geohash: String,
|
geohash: String,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
LoadCityName(geohashStr = geohash) { cityName ->
|
||||||
|
Text(
|
||||||
var cityName by remember(geohash) { mutableStateOf<String>(geohash) }
|
cityName,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
LaunchedEffect(key1 = geohash) {
|
modifier = modifier,
|
||||||
launch(Dispatchers.IO) {
|
)
|
||||||
val newCityName =
|
|
||||||
ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)?.ifBlank {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
if (newCityName != null && newCityName != cityName) {
|
|
||||||
cityName = "$newCityName ($geohash)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
|
||||||
cityName,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
Reference in New Issue
Block a user