mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
android: Connection info
This commit is contained in:
@@ -63,6 +63,9 @@ class ChatModel(val controller: ChatController) {
|
||||
val showCallView = mutableStateOf(false)
|
||||
val switchingCall = mutableStateOf(false)
|
||||
|
||||
// currently showing QR code
|
||||
val connReqInv = mutableStateOf(null as String?)
|
||||
|
||||
// working with external intents
|
||||
val sharedContent = mutableStateOf(null as SharedContent?)
|
||||
|
||||
@@ -283,6 +286,14 @@ class ChatModel(val controller: ChatController) {
|
||||
chats.add(index = 0, chat)
|
||||
}
|
||||
|
||||
fun dismissConnReqView(id: String) {
|
||||
if (connReqInv.value == null) return
|
||||
val info = getChat(id)?.chatInfo as? ChatInfo.ContactConnection ?: return
|
||||
if (info.contactConnection.connReqInv == connReqInv.value) {
|
||||
ModalManager.shared.closeModals()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChat(id: String) {
|
||||
chats.removeAll { it.id == id }
|
||||
}
|
||||
@@ -878,6 +889,8 @@ class PendingContactConnection(
|
||||
val pccConnStatus: ConnStatus,
|
||||
val viaContactUri: Boolean,
|
||||
val customUserProfileId: Long? = null,
|
||||
val connReqInv: String? = null,
|
||||
override val localAlias: String,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant
|
||||
): SomeChat, NamedChat {
|
||||
@@ -902,7 +915,6 @@ class PendingContactConnection(
|
||||
}
|
||||
override val fullName get() = ""
|
||||
override val image get() = null
|
||||
override val localAlias get() = ""
|
||||
|
||||
val initiated get() = (pccConnStatus.initiated ?: false) && !viaContactUri
|
||||
|
||||
@@ -927,6 +939,7 @@ class PendingContactConnection(
|
||||
pccAgentConnId = "abcd",
|
||||
pccConnStatus = status,
|
||||
viaContactUri = viaContactUri,
|
||||
localAlias = "",
|
||||
customUserProfileId = null,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now()
|
||||
|
||||
@@ -597,6 +597,13 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSetConnectionAlias(connId: Long, localAlias: String): PendingContactConnection? {
|
||||
val r = sendCmd(CC.ApiSetConnectionAlias(connId, localAlias))
|
||||
if (r is CR.ConnectionAliasUpdated) return r.toConnection
|
||||
Log.e(TAG, "apiSetConnectionAlias bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiCreateUserAddress(): String? {
|
||||
val r = sendCmd(CC.CreateMyAddress())
|
||||
return when (r) {
|
||||
@@ -846,12 +853,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
is CR.ContactConnected -> {
|
||||
chatModel.updateContact(r.contact)
|
||||
chatModel.dismissConnReqView(r.contact.activeConn.id)
|
||||
chatModel.removeChat(r.contact.activeConn.id)
|
||||
chatModel.updateNetworkStatus(r.contact.id, Chat.NetworkStatus.Connected())
|
||||
ntfManager.notifyContactConnected(r.contact)
|
||||
}
|
||||
is CR.ContactConnecting -> {
|
||||
chatModel.updateContact(r.contact)
|
||||
chatModel.dismissConnReqView(r.contact.activeConn.id)
|
||||
chatModel.removeChat(r.contact.activeConn.id)
|
||||
}
|
||||
is CR.ReceivedContactRequest -> {
|
||||
@@ -1340,6 +1349,7 @@ sealed class CC {
|
||||
class ApiUpdateProfile(val profile: Profile): CC()
|
||||
class ApiParseMarkdown(val text: String): CC()
|
||||
class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC()
|
||||
class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC()
|
||||
class CreateMyAddress: CC()
|
||||
class DeleteMyAddress: CC()
|
||||
class ShowMyAddress: CC()
|
||||
@@ -1394,6 +1404,7 @@ sealed class CC {
|
||||
is ApiUpdateProfile -> "/_profile ${json.encodeToString(profile)}"
|
||||
is ApiParseMarkdown -> "/_parse $text"
|
||||
is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}"
|
||||
is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}"
|
||||
is CreateMyAddress -> "/address"
|
||||
is DeleteMyAddress -> "/delete_address"
|
||||
is ShowMyAddress -> "/show_address"
|
||||
@@ -1449,6 +1460,7 @@ sealed class CC {
|
||||
is ApiUpdateProfile -> "updateProfile"
|
||||
is ApiParseMarkdown -> "apiParseMarkdown"
|
||||
is ApiSetContactAlias -> "apiSetContactAlias"
|
||||
is ApiSetConnectionAlias -> "apiSetConnectionAlias"
|
||||
is CreateMyAddress -> "createMyAddress"
|
||||
is DeleteMyAddress -> "deleteMyAddress"
|
||||
is ShowMyAddress -> "showMyAddress"
|
||||
@@ -1637,6 +1649,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("userProfileNoChange") class UserProfileNoChange: CR()
|
||||
@Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR()
|
||||
@Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val toContact: Contact): CR()
|
||||
@Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val toConnection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List<FormattedText>? = null): CR()
|
||||
@Serializable @SerialName("userContactLink") class UserContactLink(val connReqContact: String): CR()
|
||||
@Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val connReqContact: String): CR()
|
||||
@@ -1725,6 +1738,7 @@ sealed class CR {
|
||||
is UserProfileNoChange -> "userProfileNoChange"
|
||||
is UserProfileUpdated -> "userProfileUpdated"
|
||||
is ContactAliasUpdated -> "contactAliasUpdated"
|
||||
is ConnectionAliasUpdated -> "connectionAliasUpdated"
|
||||
is ParsedMarkdown -> "apiParsedMarkdown"
|
||||
is UserContactLink -> "userContactLink"
|
||||
is UserContactLinkCreated -> "userContactLinkCreated"
|
||||
@@ -1811,6 +1825,7 @@ sealed class CR {
|
||||
is UserProfileNoChange -> noDetails()
|
||||
is UserProfileUpdated -> json.encodeToString(toProfile)
|
||||
is ContactAliasUpdated -> json.encodeToString(toContact)
|
||||
is ConnectionAliasUpdated -> json.encodeToString(toConnection)
|
||||
is ParsedMarkdown -> json.encodeToString(formattedText)
|
||||
is UserContactLink -> connReqContact
|
||||
is UserContactLinkCreated -> connReqContact
|
||||
|
||||
@@ -208,7 +208,7 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit) {
|
||||
fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit) {
|
||||
var value by rememberSaveable { mutableStateOf(initialValue) }
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
DefaultBasicTextField(
|
||||
|
||||
+8
-4
@@ -63,7 +63,10 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
is ChatInfo.ContactConnection ->
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ContactConnectionView(chat.chatInfo.contactConnection) },
|
||||
click = { contactConnectionAlertDialog(chat.chatInfo.contactConnection, chatModel) },
|
||||
click = { chat.chatInfo.contactConnection.connReqInv.let {
|
||||
if (it == null) contactConnectionAlertDialog(chat.chatInfo.contactConnection, chatModel)
|
||||
else ModalManager.shared.showModalCloseable { close -> ContactConnectionInfoView(chatModel, it, chat.chatInfo.contactConnection, close) }
|
||||
} },
|
||||
dropdownMenuItems = { ContactConnectionMenuItems(chat.chatInfo, chatModel, showMenu) },
|
||||
showMenu,
|
||||
stopped
|
||||
@@ -268,7 +271,7 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
|
||||
stringResource(R.string.delete_verb),
|
||||
Icons.Outlined.Delete,
|
||||
onClick = {
|
||||
deleteContactConnectionAlert(chatInfo.contactConnection, chatModel)
|
||||
deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {}
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
@@ -337,7 +340,7 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContactConnectionAlert(connection, chatModel)
|
||||
deleteContactConnectionAlert(connection, chatModel) {}
|
||||
}) {
|
||||
Text(stringResource(R.string.delete_verb))
|
||||
}
|
||||
@@ -350,7 +353,7 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel: ChatModel) {
|
||||
fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.delete_pending_connection__question),
|
||||
text = generalGetString(
|
||||
@@ -363,6 +366,7 @@ fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel
|
||||
AlertManager.shared.hideAlert()
|
||||
if (chatModel.controller.apiDeleteChat(ChatType.ContactConnection, connection.apiId)) {
|
||||
chatModel.removeChat(connection.id)
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-6
@@ -4,16 +4,14 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AddLink
|
||||
import androidx.compose.material.icons.outlined.Link
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.model.PendingContactConnection
|
||||
import chat.simplex.app.model.getTimestampText
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ProfileImage
|
||||
|
||||
@@ -21,7 +19,16 @@ import chat.simplex.app.views.helpers.ProfileImage
|
||||
fun ContactConnectionView(contactConnection: PendingContactConnection) {
|
||||
Row {
|
||||
Box(Modifier.size(72.dp), contentAlignment = Alignment.Center) {
|
||||
ProfileImage(size = 54.dp, null, if (contactConnection.initiated) Icons.Outlined.AddLink else Icons.Outlined.Link)
|
||||
ProfileImage(
|
||||
size = 54.dp,
|
||||
null,
|
||||
if (contactConnection.initiated) Icons.Outlined.QrCode else Icons.Outlined.Link,
|
||||
color = if (contactConnection.connReqInv == null)
|
||||
MaterialTheme.colors.secondary
|
||||
else if (contactConnection.incognito)
|
||||
Indigo else
|
||||
MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -63,7 +63,7 @@ class ModalManager {
|
||||
}
|
||||
|
||||
fun closeModals() {
|
||||
while (modalViews.isNotEmpty()) closeModal()
|
||||
while (modalCount.value > 0) closeModal()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package chat.simplex.app.views.chatlist
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.LocalAliasEditor
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.InfoAboutIncognito
|
||||
import chat.simplex.app.views.newchat.QRCode
|
||||
|
||||
@Composable
|
||||
fun ContactConnectionInfoView(chatModel: ChatModel, connReqInvitation: String, contactConnection: PendingContactConnection, close: () -> Unit) {
|
||||
val cxt = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
if (contactConnection.connReqInv != null) {
|
||||
chatModel.connReqInv.value = contactConnection.connReqInv
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { chatModel.connReqInv.value = null }
|
||||
}
|
||||
ContactConnectionInfoLayout(
|
||||
chatModelIncognito = chatModel.incognito.value,
|
||||
connReq = connReqInvitation,
|
||||
contactConnection.localAlias,
|
||||
share = { shareText(cxt, connReqInvitation) },
|
||||
deleteConnection = { deleteContactConnectionAlert(contactConnection, chatModel, close) },
|
||||
onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactConnectionInfoLayout(
|
||||
chatModelIncognito: Boolean,
|
||||
connReq: String,
|
||||
localAlias: String,
|
||||
share: () -> Unit,
|
||||
deleteConnection: () -> Unit,
|
||||
onLocalAliasChanged: (String) -> Unit
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
val screenHeight = maxHeight
|
||||
var showQr by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING , bottom = DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.shared_one_time_link), false)
|
||||
|
||||
LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged)
|
||||
|
||||
Text(
|
||||
stringResource(R.string.show_QR_code_for_your_contact_to_scan_from_the_app__multiline),
|
||||
)
|
||||
Row {
|
||||
InfoAboutIncognito(
|
||||
chatModelIncognito,
|
||||
true,
|
||||
generalGetString(R.string.incognito_random_profile_description),
|
||||
generalGetString(R.string.your_profile_will_be_sent)
|
||||
)
|
||||
}
|
||||
if (connReq.isNotEmpty() && showQr) {
|
||||
QRCode(
|
||||
connReq, Modifier
|
||||
.aspectRatio(1f)
|
||||
.padding(vertical = 3.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
annotatedStringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel),
|
||||
lineHeight = 22.sp,
|
||||
modifier = Modifier
|
||||
.padding(bottom = if (screenHeight > 600.dp) 8.dp else 0.dp)
|
||||
)
|
||||
}
|
||||
if (!showQr) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
SimpleButton(stringResource(R.string.show_QR_code), icon = Icons.Outlined.QrCode, click = { showQr = true })
|
||||
}
|
||||
}
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
SimpleButton(stringResource(R.string.share_invitation_link), icon = Icons.Outlined.Share, click = share)
|
||||
}
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
SimpleButton(stringResource(R.string.delete_verb), icon = Icons.Outlined.Delete, color = Color.Red, click = deleteConnection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setContactAlias(contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withApi {
|
||||
chatModel.controller.apiSetConnectionAlias(contactConnection.pccConnId, localAlias)?.let {
|
||||
chatModel.updateContactConnection(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
private fun PreviewAddContactView() {
|
||||
SimpleXTheme {
|
||||
ContactConnectionInfoLayout(
|
||||
chatModelIncognito = false,
|
||||
localAlias = "",
|
||||
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
|
||||
share = {},
|
||||
deleteConnection = {},
|
||||
onLocalAliasChanged = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,14 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
|
||||
createInvitation(m, creatingConnReq, connReqInvitation)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(connReqInvitation.value) {
|
||||
if (connReqInvitation.value.isNotEmpty()) {
|
||||
m.connReqInv.value = connReqInvitation.value
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { m.connReqInv.value = null }
|
||||
}
|
||||
val tabTitles = CreateLinkTab.values().map {
|
||||
when {
|
||||
it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() -> stringResource(R.string.create_one_time_link)
|
||||
|
||||
@@ -307,6 +307,10 @@
|
||||
<string name="icon_descr_email">E-Mail</string>
|
||||
<string name="icon_descr_more_button">Mehr</string>
|
||||
|
||||
<!-- Connection info - ContactConnectionInfoView.kt -->
|
||||
<string name="show_QR_code">Show QR code</string>
|
||||
<string name="shared_one_time_link">Shared one-time link</string>
|
||||
|
||||
<!-- Add Contact - AddContactView.kt -->
|
||||
<string name="invalid_QR_code">Ungültiger QR-Code</string>
|
||||
<string name="this_QR_code_is_not_a_link">Dieser QR-Code beschreibt keinen Link!</string>
|
||||
|
||||
@@ -307,6 +307,10 @@
|
||||
<string name="icon_descr_email">Email</string>
|
||||
<string name="icon_descr_more_button">Больше</string>
|
||||
|
||||
<!-- Connection info - ContactConnectionInfoView.kt -->
|
||||
<string name="show_QR_code">Показать QR код</string>
|
||||
<string name="shared_one_time_link">Поделиться одноразовой ссылкой</string>
|
||||
|
||||
<!-- Add Contact - AddContactView.kt -->
|
||||
<string name="invalid_QR_code">Неверный QR код</string>
|
||||
<string name="this_QR_code_is_not_a_link">Этот QR код не является ссылкой!</string>
|
||||
|
||||
@@ -307,6 +307,10 @@
|
||||
<string name="icon_descr_email">Email</string>
|
||||
<string name="icon_descr_more_button">More</string>
|
||||
|
||||
<!-- Connection info - ContactConnectionInfoView.kt -->
|
||||
<string name="show_QR_code">Show QR code</string>
|
||||
<string name="shared_one_time_link">Shared one-time link</string>
|
||||
|
||||
<!-- Add Contact - AddContactView.kt -->
|
||||
<string name="invalid_QR_code">Invalid QR code</string>
|
||||
<string name="this_QR_code_is_not_a_link">This QR code is not a link!</string>
|
||||
@@ -315,7 +319,7 @@
|
||||
<string name="connection_request_sent">Connection request sent!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">You will be connected when your connection request is accepted, please wait or check later!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">You will be connected when your contact\'s device is online, please wait or check later!</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Your contact can scan it from the app.</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Your contact can scan QR code from the app.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">If you can\'t meet in person, <b>show QR code in the video call</b>, or share the link.</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Your chat profile will be sent\nto your contact</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">If you cannot meet in person, you can <b>scan QR code in the video call</b>, or your contact can share an invitation link.</string>
|
||||
|
||||
Reference in New Issue
Block a user