mirror of
https://github.com/oxen-io/session-android.git
synced 2025-04-30 14:20:47 +00:00
Fix control messages
This commit is contained in:
parent
8cc26b8fb6
commit
7bd43b1b3c
@ -61,7 +61,7 @@ class DisappearingMessagesViewModel(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val expiryMode = storage.getExpirationConfiguration(threadId)?.expiryMode ?: ExpiryMode.NONE
|
val expiryMode = storage.getExpirationConfiguration(threadId)?.expiryMode?.maybeConvertToLegacy(isNewConfigEnabled) ?: ExpiryMode.NONE
|
||||||
val recipient = threadDb.getRecipientForThreadId(threadId)
|
val recipient = threadDb.getRecipientForThreadId(threadId)
|
||||||
val groupRecord = recipient?.takeIf { it.isClosedGroupRecipient }
|
val groupRecord = recipient?.takeIf { it.isClosedGroupRecipient }
|
||||||
?.run { groupDb.getGroup(address.toGroupString()).orNull() }
|
?.run { groupDb.getGroup(address.toGroupString()).orNull() }
|
||||||
@ -83,7 +83,7 @@ class DisappearingMessagesViewModel(
|
|||||||
|
|
||||||
override fun onSetClick() = viewModelScope.launch {
|
override fun onSetClick() = viewModelScope.launch {
|
||||||
val state = _state.value
|
val state = _state.value
|
||||||
val mode = state.expiryMode
|
val mode = state.expiryMode?.coerceLegacyToAfterSend()
|
||||||
val address = state.address
|
val address = state.address
|
||||||
if (address == null || mode == null) {
|
if (address == null || mode == null) {
|
||||||
_event.send(Event.FAIL)
|
_event.send(Event.FAIL)
|
||||||
@ -93,8 +93,9 @@ class DisappearingMessagesViewModel(
|
|||||||
val expiryChangeTimestampMs = SnodeAPI.nowWithOffset
|
val expiryChangeTimestampMs = SnodeAPI.nowWithOffset
|
||||||
storage.setExpirationConfiguration(ExpirationConfiguration(threadId, mode, expiryChangeTimestampMs))
|
storage.setExpirationConfiguration(ExpirationConfiguration(threadId, mode, expiryChangeTimestampMs))
|
||||||
|
|
||||||
val message = ExpirationTimerUpdate(mode.expirySeconds.toInt()).apply {
|
val message = ExpirationTimerUpdate(mode).apply {
|
||||||
sender = textSecurePreferences.getLocalNumber()
|
sender = textSecurePreferences.getLocalNumber()
|
||||||
|
isSenderSelf = true
|
||||||
recipient = address.serialize()
|
recipient = address.serialize()
|
||||||
sentTimestamp = expiryChangeTimestampMs
|
sentTimestamp = expiryChangeTimestampMs
|
||||||
}
|
}
|
||||||
@ -106,6 +107,8 @@ class DisappearingMessagesViewModel(
|
|||||||
_event.send(Event.SUCCESS)
|
_event.send(Event.SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ExpiryMode.coerceLegacyToAfterSend() = takeUnless { it is ExpiryMode.Legacy } ?: ExpiryMode.AfterSend(expirySeconds)
|
||||||
|
|
||||||
@dagger.assisted.AssistedFactory
|
@dagger.assisted.AssistedFactory
|
||||||
interface AssistedFactory {
|
interface AssistedFactory {
|
||||||
fun create(threadId: Long): Factory
|
fun create(threadId: Long): Factory
|
||||||
@ -135,3 +138,5 @@ class DisappearingMessagesViewModel(
|
|||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ExpiryMode.maybeConvertToLegacy(isNewConfigEnabled: Boolean): ExpiryMode = takeIf { isNewConfigEnabled } ?: ExpiryMode.Legacy(expirySeconds)
|
||||||
|
@ -3,8 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2.components
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.AnimationDrawable
|
import android.graphics.drawable.AnimationDrawable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.Log
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
@ -30,6 +30,7 @@ import network.loki.messenger.databinding.ViewVisibleMessageBinding
|
|||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
import org.session.libsession.messaging.contacts.Contact.ContactContext
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||||
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.ViewUtil
|
import org.session.libsession.utilities.ViewUtil
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
@ -348,23 +349,18 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateExpirationTimer(message: MessageRecord) {
|
private fun updateExpirationTimer(message: MessageRecord) {
|
||||||
val expirationTimerView = binding.expirationTimerView
|
|
||||||
|
|
||||||
if (!message.isOutgoing) binding.messageStatusTextView.bringToFront()
|
if (!message.isOutgoing) binding.messageStatusTextView.bringToFront()
|
||||||
|
|
||||||
if (message.expiresIn > 0) {
|
val expireStarted = message.expireStarted.takeIf { it > 0 } ?: SnodeAPI.nowWithOffset
|
||||||
if (message.expireStarted > 0) {
|
|
||||||
expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
|
val id = message.getId()
|
||||||
ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule()
|
val mms = message.isMms
|
||||||
} else {
|
binding.expirationTimerView.setExpirationTime(expireStarted, message.expiresIn)
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager
|
val db = if (mms) mmsDb else smsDb
|
||||||
val id = message.getId()
|
db.markExpireStarted(id, expireStarted)
|
||||||
val mms = message.isMms
|
ApplicationContext.getInstance(context).expiringMessageManager
|
||||||
if (mms) mmsDb.markExpireStarted(id) else smsDb.markExpireStarted(id)
|
.scheduleDeletion(id, mms, expireStarted, message.expiresIn)
|
||||||
expirationManager.scheduleDeletion(id, mms, message.expiresIn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
data class ExpirationInfo(val id: Long, val expiresIn: Long, val expireStarted: Long, val isMms: Boolean)
|
@ -0,0 +1,14 @@
|
|||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
|
||||||
|
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId
|
||||||
|
|
||||||
|
data class MarkedMessageInfo(val syncMessageId: SyncMessageId, val expirationInfo: ExpirationInfo) {
|
||||||
|
fun guessExpiryType(): ExpiryType = expirationInfo.run {
|
||||||
|
when {
|
||||||
|
syncMessageId.timetamp == expireStarted -> ExpiryType.AFTER_SEND
|
||||||
|
expiresIn > 0 -> ExpiryType.AFTER_READ
|
||||||
|
else -> ExpiryType.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,6 @@ import org.session.libsession.utilities.Document;
|
|||||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||||
import org.session.libsession.utilities.IdentityKeyMismatchList;
|
import org.session.libsession.utilities.IdentityKeyMismatchList;
|
||||||
import org.session.libsignal.crypto.IdentityKey;
|
import org.session.libsignal.crypto.IdentityKey;
|
||||||
import org.session.libsignal.protos.SignalServiceProtos;
|
|
||||||
import org.session.libsignal.utilities.JsonUtil;
|
import org.session.libsignal.utilities.JsonUtil;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType;
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType;
|
||||||
@ -227,66 +226,6 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ExpirationInfo {
|
|
||||||
|
|
||||||
private final long id;
|
|
||||||
private final long expiresIn;
|
|
||||||
private final long expireStarted;
|
|
||||||
private final boolean mms;
|
|
||||||
|
|
||||||
public ExpirationInfo(long id, long expiresIn, long expireStarted, boolean mms) {
|
|
||||||
this.id = id;
|
|
||||||
this.expiresIn = expiresIn;
|
|
||||||
this.expireStarted = expireStarted;
|
|
||||||
this.mms = mms;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getExpiresIn() {
|
|
||||||
return expiresIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getExpireStarted() {
|
|
||||||
return expireStarted;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMms() {
|
|
||||||
return mms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class MarkedMessageInfo {
|
|
||||||
|
|
||||||
private final SyncMessageId syncMessageId;
|
|
||||||
private final ExpirationInfo expirationInfo;
|
|
||||||
|
|
||||||
public MarkedMessageInfo(SyncMessageId syncMessageId, ExpirationInfo expirationInfo) {
|
|
||||||
this.syncMessageId = syncMessageId;
|
|
||||||
this.expirationInfo = expirationInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SyncMessageId getSyncMessageId() {
|
|
||||||
return syncMessageId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExpirationInfo getExpirationInfo() {
|
|
||||||
return expirationInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExpiryType guessExpiryType() {
|
|
||||||
long expireStarted = expirationInfo.expireStarted;
|
|
||||||
long expiresIn = expirationInfo.expiresIn;
|
|
||||||
long timestamp = syncMessageId.timetamp;
|
|
||||||
|
|
||||||
if (timestamp == expireStarted) return ExpiryType.AFTER_SEND;
|
|
||||||
if (expiresIn > 0) return ExpiryType.AFTER_READ;
|
|
||||||
return ExpiryType.NONE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class InsertResult {
|
public static class InsertResult {
|
||||||
private final long messageId;
|
private final long messageId;
|
||||||
private final long threadId;
|
private final long threadId;
|
||||||
|
@ -1718,9 +1718,7 @@ open class Storage(
|
|||||||
?.run { disappearingTimer.takeIf { it != 0L }?.let(ExpiryMode::AfterSend) ?: ExpiryMode.NONE }
|
?.run { disappearingTimer.takeIf { it != 0L }?.let(ExpiryMode::AfterSend) ?: ExpiryMode.NONE }
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}?.let { ExpirationConfiguration(threadId, it, dbExpirationMetadata.updatedTimestampMs) }
|
||||||
?.run { takeIf { isNewConfigEnabled || it is ExpiryMode.NONE } ?: ExpiryMode.Legacy(expirySeconds) }
|
|
||||||
?.let { ExpirationConfiguration(threadId, it, dbExpirationMetadata.updatedTimestampMs) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setExpirationConfiguration(config: ExpirationConfiguration) {
|
override fun setExpirationConfiguration(config: ExpirationConfiguration) {
|
||||||
@ -1729,12 +1727,7 @@ open class Storage(
|
|||||||
val expirationDb = DatabaseComponent.get(context).expirationConfigurationDatabase()
|
val expirationDb = DatabaseComponent.get(context).expirationConfigurationDatabase()
|
||||||
val currentConfig = expirationDb.getExpirationConfiguration(config.threadId)
|
val currentConfig = expirationDb.getExpirationConfiguration(config.threadId)
|
||||||
if (currentConfig != null && currentConfig.updatedTimestampMs >= config.updatedTimestampMs) return
|
if (currentConfig != null && currentConfig.updatedTimestampMs >= config.updatedTimestampMs) return
|
||||||
|
val expiryMode = config.expiryMode
|
||||||
val expiryMode = config.expiryMode.run {
|
|
||||||
takeUnless { it is ExpiryMode.Legacy }
|
|
||||||
?: if (recipient.isContactRecipient) ExpiryMode.AfterRead(expirySeconds)
|
|
||||||
else ExpiryMode.AfterSend(expirySeconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recipient.isClosedGroupRecipient) {
|
if (recipient.isClosedGroupRecipient) {
|
||||||
val userGroups = configFactory.userGroups ?: return
|
val userGroups = configFactory.userGroups ?: return
|
||||||
|
@ -51,7 +51,6 @@ import org.session.libsignal.utilities.Pair;
|
|||||||
import org.session.libsignal.utilities.guava.Optional;
|
import org.session.libsignal.utilities.guava.Optional;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactUtil;
|
import org.thoughtcrime.securesms.contacts.ContactUtil;
|
||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
|
@ -27,7 +27,7 @@ import androidx.core.app.NotificationManagerCompat;
|
|||||||
|
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
import org.thoughtcrime.securesms.database.MarkedMessageInfo;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -36,7 +36,7 @@ import org.session.libsession.utilities.Address;
|
|||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
import org.thoughtcrime.securesms.database.MarkedMessageInfo;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
|
|
||||||
|
@ -18,8 +18,8 @@ import org.session.libsession.utilities.recipients.Recipient
|
|||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
|
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
|
||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.ExpirationInfo
|
import org.thoughtcrime.securesms.database.ExpirationInfo
|
||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo
|
import org.thoughtcrime.securesms.database.MarkedMessageInfo
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
import org.thoughtcrime.securesms.util.SessionMetaProtocol.shouldSendReadReceipt
|
import org.thoughtcrime.securesms.util.SessionMetaProtocol.shouldSendReadReceipt
|
||||||
|
|
||||||
@ -129,16 +129,18 @@ class MarkReadReceiver : BroadcastReceiver() {
|
|||||||
expirationInfo: ExpirationInfo,
|
expirationInfo: ExpirationInfo,
|
||||||
expiresIn: Long = expirationInfo.expiresIn
|
expiresIn: Long = expirationInfo.expiresIn
|
||||||
) {
|
) {
|
||||||
if (expiresIn > 0 && expirationInfo.expireStarted <= 0) {
|
if (expiresIn <= 0 || expirationInfo.expireStarted > 0) return
|
||||||
if (expirationInfo.isMms) DatabaseComponent.get(context!!).mmsDatabase().markExpireStarted(expirationInfo.id)
|
|
||||||
else DatabaseComponent.get(context!!).smsDatabase().markExpireStarted(expirationInfo.id)
|
|
||||||
|
|
||||||
ApplicationContext.getInstance(context).expiringMessageManager.scheduleDeletion(
|
val now = SnodeAPI.nowWithOffset
|
||||||
expirationInfo.id,
|
val db = DatabaseComponent.get(context!!).run { if (expirationInfo.isMms) mmsDatabase() else smsDatabase() }
|
||||||
expirationInfo.isMms,
|
db.markExpireStarted(expirationInfo.id, now)
|
||||||
expiresIn
|
|
||||||
)
|
ApplicationContext.getInstance(context).expiringMessageManager.scheduleDeletion(
|
||||||
}
|
expirationInfo.id,
|
||||||
|
expirationInfo.isMms,
|
||||||
|
now,
|
||||||
|
expiresIn
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ import org.session.libsession.utilities.Address;
|
|||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsignal.utilities.Log;
|
import org.session.libsignal.utilities.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
import org.thoughtcrime.securesms.database.MarkedMessageInfo;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.Storage;
|
import org.thoughtcrime.securesms.database.Storage;
|
||||||
|
@ -9,6 +9,7 @@ import org.session.libsession.messaging.messages.ExpirationConfiguration;
|
|||||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
|
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
|
||||||
|
import org.session.libsession.snode.SnodeAPI;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
import org.session.libsession.utilities.GroupUtil;
|
import org.session.libsession.utilities.GroupUtil;
|
||||||
import org.session.libsession.utilities.SSKEnvironment;
|
import org.session.libsession.utilities.SSKEnvironment;
|
||||||
@ -54,10 +55,6 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
executor.execute(new ProcessTask());
|
executor.execute(new ProcessTask());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void scheduleDeletion(long id, boolean mms, long expiresInMillis) {
|
|
||||||
scheduleDeletion(id, mms, System.currentTimeMillis(), expiresInMillis);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void scheduleDeletion(long id, boolean mms, long startedAtTimestamp, long expiresInMillis) {
|
public void scheduleDeletion(long id, boolean mms, long startedAtTimestamp, long expiresInMillis) {
|
||||||
long expiresAtMillis = startedAtTimestamp + expiresInMillis;
|
long expiresAtMillis = startedAtTimestamp + expiresInMillis;
|
||||||
|
|
||||||
@ -78,8 +75,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||||
String senderPublicKey = message.getSender();
|
String senderPublicKey = message.getSender();
|
||||||
long sentTimestamp = message.getSentTimestamp() == null ? 0 : message.getSentTimestamp();
|
long sentTimestamp = message.getSentTimestamp() == null ? 0 : message.getSentTimestamp();
|
||||||
long expireStartedAt = (expiryMode instanceof ExpiryMode.AfterSend)
|
long expireStartedAt = (expiryMode instanceof ExpiryMode.AfterSend || message.isSenderSelf()) ? sentTimestamp : 0;
|
||||||
? sentTimestamp : 0;
|
|
||||||
|
|
||||||
// Notify the user
|
// Notify the user
|
||||||
if (senderPublicKey == null || userPublicKey.equals(senderPublicKey)) {
|
if (senderPublicKey == null || userPublicKey.equals(senderPublicKey)) {
|
||||||
@ -88,7 +84,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
} else {
|
} else {
|
||||||
insertIncomingExpirationTimerMessage(message, expireStartedAt);
|
insertIncomingExpirationTimerMessage(message, expireStartedAt);
|
||||||
}
|
}
|
||||||
if (expiryMode instanceof ExpiryMode.AfterSend && message.getSentTimestamp() != null && senderPublicKey != null) {
|
if (expiryMode.getExpirySeconds() > 0 && message.getSentTimestamp() != null && senderPublicKey != null) {
|
||||||
startAnyExpiration(message.getSentTimestamp(), senderPublicKey, expireStartedAt);
|
startAnyExpiration(message.getSentTimestamp(), senderPublicKey, expireStartedAt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,7 +94,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
String senderPublicKey = message.getSender();
|
String senderPublicKey = message.getSender();
|
||||||
Long sentTimestamp = message.getSentTimestamp();
|
Long sentTimestamp = message.getSentTimestamp();
|
||||||
String groupId = message.getGroupPublicKey();
|
String groupId = message.getGroupPublicKey();
|
||||||
long expiresInMillis = message.getDuration() * 1000L;
|
long expiresInMillis = message.getExpiryMode().getExpiryMillis();
|
||||||
|
|
||||||
Optional<SignalServiceGroup> groupInfo = Optional.absent();
|
Optional<SignalServiceGroup> groupInfo = Optional.absent();
|
||||||
Address address = Address.fromSerialized(senderPublicKey);
|
Address address = Address.fromSerialized(senderPublicKey);
|
||||||
@ -144,7 +140,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
|
|
||||||
Long sentTimestamp = message.getSentTimestamp();
|
Long sentTimestamp = message.getSentTimestamp();
|
||||||
String groupId = message.getGroupPublicKey();
|
String groupId = message.getGroupPublicKey();
|
||||||
int duration = message.getDuration();
|
long duration = message.getExpiryMode().getExpiryMillis();
|
||||||
|
|
||||||
Address address;
|
Address address;
|
||||||
|
|
||||||
@ -159,7 +155,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
StorageProtocol storage = MessagingModuleConfiguration.getShared().getStorage();
|
StorageProtocol storage = MessagingModuleConfiguration.getShared().getStorage();
|
||||||
message.setThreadID(storage.getOrCreateThreadIdFor(address));
|
message.setThreadID(storage.getOrCreateThreadIdFor(address));
|
||||||
|
|
||||||
OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, expireStartedAt, groupId);
|
OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration, expireStartedAt, groupId);
|
||||||
mmsDatabase.insertSecureDecryptedMessageOutbox(timerUpdateMessage, message.getThreadID(), sentTimestamp, true);
|
mmsDatabase.insertSecureDecryptedMessageOutbox(timerUpdateMessage, message.getThreadID(), sentTimestamp, true);
|
||||||
} catch (MmsException | IOException ioe) {
|
} catch (MmsException | IOException ioe) {
|
||||||
Log.e("Loki", "Failed to insert expiration update message.", ioe);
|
Log.e("Loki", "Failed to insert expiration update message.", ioe);
|
||||||
@ -169,18 +165,17 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
@Override
|
@Override
|
||||||
public void startAnyExpiration(long timestamp, @NotNull String author, long expireStartedAt) {
|
public void startAnyExpiration(long timestamp, @NotNull String author, long expireStartedAt) {
|
||||||
MessageRecord messageRecord = mmsSmsDatabase.getMessageFor(timestamp, author);
|
MessageRecord messageRecord = mmsSmsDatabase.getMessageFor(timestamp, author);
|
||||||
if (messageRecord != null) {
|
if (messageRecord == null) return;
|
||||||
boolean mms = messageRecord.isMms();
|
boolean mms = messageRecord.isMms();
|
||||||
ExpirationConfiguration config = DatabaseComponent.get(context).storage().getExpirationConfiguration(messageRecord.getThreadId());
|
ExpirationConfiguration config = DatabaseComponent.get(context).storage().getExpirationConfiguration(messageRecord.getThreadId());
|
||||||
if (config == null || !config.isEnabled()) return;
|
if (config == null || !config.isEnabled()) return;
|
||||||
ExpiryMode mode = config.getExpiryMode();
|
ExpiryMode mode = config.getExpiryMode();
|
||||||
if (mms) {
|
if (mms) {
|
||||||
mmsDatabase.markExpireStarted(messageRecord.getId(), expireStartedAt);
|
mmsDatabase.markExpireStarted(messageRecord.getId(), expireStartedAt);
|
||||||
} else {
|
} else {
|
||||||
smsDatabase.markExpireStarted(messageRecord.getId(), expireStartedAt);
|
smsDatabase.markExpireStarted(messageRecord.getId(), expireStartedAt);
|
||||||
}
|
|
||||||
scheduleDeletion(messageRecord.getId(), mms, expireStartedAt, (mode != null ? mode.getExpiryMillis() : 0));
|
|
||||||
}
|
}
|
||||||
|
scheduleDeletion(messageRecord.getId(), mms, expireStartedAt, (mode != null ? mode.getExpiryMillis() : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LoadTask implements Runnable {
|
private class LoadTask implements Runnable {
|
||||||
@ -219,7 +214,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
while (expiringMessageReferences.isEmpty()) expiringMessageReferences.wait();
|
while (expiringMessageReferences.isEmpty()) expiringMessageReferences.wait();
|
||||||
|
|
||||||
ExpiringMessageReference nextReference = expiringMessageReferences.first();
|
ExpiringMessageReference nextReference = expiringMessageReferences.first();
|
||||||
long waitTime = nextReference.expiresAtMillis - System.currentTimeMillis();
|
long waitTime = nextReference.expiresAtMillis - SnodeAPI.getNowWithOffset();
|
||||||
|
|
||||||
if (waitTime > 0) {
|
if (waitTime > 0) {
|
||||||
ExpirationListener.setAlarm(context, waitTime);
|
ExpirationListener.setAlarm(context, waitTime);
|
||||||
|
@ -64,13 +64,11 @@ abstract class Message {
|
|||||||
}
|
}
|
||||||
expirationTimer = config.expiryMode.expirySeconds.toInt()
|
expirationTimer = config.expiryMode.expirySeconds.toInt()
|
||||||
lastDisappearingMessageChangeTimestamp = config.updatedTimestampMs
|
lastDisappearingMessageChangeTimestamp = config.updatedTimestampMs
|
||||||
if (ExpirationConfiguration.isNewConfigEnabled) {
|
config.expiryMode.let { expiryMode ->
|
||||||
config.expiryMode.let { expiryMode ->
|
expirationType = when (expiryMode) {
|
||||||
when (expiryMode) {
|
is ExpiryMode.Legacy, is ExpiryMode.AfterSend -> ExpirationType.DELETE_AFTER_SEND
|
||||||
is ExpiryMode.AfterSend -> expirationType = ExpirationType.DELETE_AFTER_SEND
|
is ExpiryMode.AfterRead -> ExpirationType.DELETE_AFTER_READ
|
||||||
is ExpiryMode.AfterRead -> expirationType = ExpirationType.DELETE_AFTER_READ
|
ExpiryMode.NONE -> ExpirationType.UNKNOWN
|
||||||
is ExpiryMode.Legacy, ExpiryMode.NONE -> { /* do nothing */ }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.session.libsession.messaging.messages.control
|
package org.session.libsession.messaging.messages.control
|
||||||
|
|
||||||
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
@ -10,15 +11,10 @@ import org.session.libsignal.utilities.Log
|
|||||||
*
|
*
|
||||||
* **Note:** `nil` if this isn't a sync message.
|
* **Note:** `nil` if this isn't a sync message.
|
||||||
*/
|
*/
|
||||||
data class ExpirationTimerUpdate(var duration: Int? = 0, var syncTarget: String? = null) : ControlMessage() {
|
data class ExpirationTimerUpdate(var expiryMode: ExpiryMode, var syncTarget: String? = null) : ControlMessage() {
|
||||||
|
|
||||||
override val isSelfSendValid: Boolean = true
|
override val isSelfSendValid: Boolean = true
|
||||||
|
|
||||||
override fun isValid(): Boolean {
|
|
||||||
if (!super.isValid()) return false
|
|
||||||
return duration != null || ExpirationConfiguration.isNewConfigEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "ExpirationTimerUpdate"
|
const val TAG = "ExpirationTimerUpdate"
|
||||||
|
|
||||||
@ -29,15 +25,22 @@ data class ExpirationTimerUpdate(var duration: Int? = 0, var syncTarget: String?
|
|||||||
) != 0
|
) != 0
|
||||||
if (!isExpirationTimerUpdate) return null
|
if (!isExpirationTimerUpdate) return null
|
||||||
val syncTarget = dataMessageProto.syncTarget
|
val syncTarget = dataMessageProto.syncTarget
|
||||||
val duration = if (proto.hasExpirationTimer()) proto.expirationTimer else dataMessageProto.expireTimer
|
val duration: Int = if (proto.hasExpirationTimer()) proto.expirationTimer else dataMessageProto.expireTimer
|
||||||
return ExpirationTimerUpdate(duration, syncTarget)
|
val type = proto.expirationType.takeIf { duration > 0 }
|
||||||
|
val expiryMode = when (type) {
|
||||||
|
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_SEND -> ExpiryMode.AfterSend(duration.toLong())
|
||||||
|
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(duration.toLong())
|
||||||
|
else -> ExpiryMode.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExpirationTimerUpdate(expiryMode, syncTarget)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toProto(): SignalServiceProtos.Content? {
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
||||||
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
|
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
|
||||||
duration?.let { dataMessageProto.expireTimer = it }
|
dataMessageProto.expireTimer = expiryMode.expirySeconds.toInt()
|
||||||
// Sync target
|
// Sync target
|
||||||
if (syncTarget != null) {
|
if (syncTarget != null) {
|
||||||
dataMessageProto.syncTarget = syncTarget
|
dataMessageProto.syncTarget = syncTarget
|
||||||
@ -53,6 +56,8 @@ data class ExpirationTimerUpdate(var duration: Int? = 0, var syncTarget: String?
|
|||||||
}
|
}
|
||||||
return try {
|
return try {
|
||||||
SignalServiceProtos.Content.newBuilder().apply {
|
SignalServiceProtos.Content.newBuilder().apply {
|
||||||
|
expirationType
|
||||||
|
expirationTimer
|
||||||
dataMessage = dataMessageProto.build()
|
dataMessage = dataMessageProto.build()
|
||||||
setExpirationConfigurationIfNeeded(threadID)
|
setExpirationConfigurationIfNeeded(threadID)
|
||||||
}.build()
|
}.build()
|
||||||
|
@ -148,12 +148,14 @@ object MessageReceiver {
|
|||||||
VisibleMessage.fromProto(proto) ?: run {
|
VisibleMessage.fromProto(proto) ?: run {
|
||||||
throw Error.UnknownMessage
|
throw Error.UnknownMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
val isUserBlindedSender = sender == openGroupPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
|
val isUserBlindedSender = sender == openGroupPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
|
||||||
|
val isUserSender = sender == userPublicKey
|
||||||
// Ignore self send if needed
|
// Ignore self send if needed
|
||||||
if (!message.isSelfSendValid && (sender == userPublicKey || isUserBlindedSender)) {
|
if (!message.isSelfSendValid && (isUserSender || isUserBlindedSender)) {
|
||||||
throw Error.SelfSend
|
throw Error.SelfSend
|
||||||
}
|
}
|
||||||
if (sender == userPublicKey || isUserBlindedSender) {
|
if (isUserSender || isUserBlindedSender) {
|
||||||
message.isSenderSelf = true
|
message.isSenderSelf = true
|
||||||
}
|
}
|
||||||
// Guard against control messages in open groups
|
// Guard against control messages in open groups
|
||||||
|
@ -40,7 +40,6 @@ import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID
|
|||||||
import org.session.libsession.utilities.ProfileKeyUtil
|
import org.session.libsession.utilities.ProfileKeyUtil
|
||||||
import org.session.libsession.utilities.SSKEnvironment
|
import org.session.libsession.utilities.SSKEnvironment
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.expiryMode
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||||
@ -156,28 +155,21 @@ fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) {
|
|||||||
private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimerUpdate) {
|
private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimerUpdate) {
|
||||||
if (ExpirationConfiguration.isNewConfigEnabled) return
|
if (ExpirationConfiguration.isNewConfigEnabled) return
|
||||||
val module = MessagingModuleConfiguration.shared
|
val module = MessagingModuleConfiguration.shared
|
||||||
val recipient = Recipient.from(module.context, Address.fromSerialized(message.sender!!), false)
|
|
||||||
val type = when {
|
|
||||||
recipient.isContactRecipient -> ExpiryMode.AfterRead(message.duration!!.toLong())
|
|
||||||
recipient.isGroupRecipient -> ExpiryMode.AfterSend(message.duration!!.toLong())
|
|
||||||
else -> ExpiryMode.NONE
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
var threadId: Long = module.storage.getOrCreateThreadIdFor(fromSerialized(message.sender!!))
|
val threadId = fromSerialized(message.groupPublicKey?.let(::doubleEncodeGroupID) ?: message.sender!!)
|
||||||
if (message.groupPublicKey != null) {
|
.let(module.storage::getOrCreateThreadIdFor)
|
||||||
threadId = module.storage.getOrCreateThreadIdFor(fromSerialized(doubleEncodeGroupID(message.groupPublicKey!!)))
|
|
||||||
}
|
|
||||||
module.storage.setExpirationConfiguration(
|
module.storage.setExpirationConfiguration(
|
||||||
ExpirationConfiguration(
|
ExpirationConfiguration(
|
||||||
threadId,
|
threadId,
|
||||||
type,
|
message.expiryMode,
|
||||||
message.sentTimestamp!!
|
message.sentTimestamp!!
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("Loki", "Failed to update expiration configuration.")
|
Log.e("Loki", "Failed to update expiration configuration.")
|
||||||
}
|
}
|
||||||
SSKEnvironment.shared.messageExpirationManager.setExpirationTimer(message, type)
|
SSKEnvironment.shared.messageExpirationManager.setExpirationTimer(message, message.expiryMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageReceiver.handleDataExtractionNotification(message: DataExtractionNotification) {
|
private fun MessageReceiver.handleDataExtractionNotification(message: DataExtractionNotification) {
|
||||||
@ -317,10 +309,18 @@ fun MessageReceiver.updateExpiryIfNeeded(
|
|||||||
|
|
||||||
|
|
||||||
if (message is ExpirationTimerUpdate) {
|
if (message is ExpirationTimerUpdate) {
|
||||||
SSKEnvironment.shared.messageExpirationManager.setExpirationTimer(message, type?.expiryMode(durationSeconds.toLong()))
|
SSKEnvironment.shared.messageExpirationManager.setExpirationTimer(message, expiryMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun SignalServiceProtos.Content.ExpirationType.expiryMode(durationSeconds: Long) = takeIf { durationSeconds > 0 }?.let {
|
||||||
|
when (it) {
|
||||||
|
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(durationSeconds)
|
||||||
|
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_SEND, SignalServiceProtos.Content.ExpirationType.UNKNOWN -> ExpiryMode.AfterSend(durationSeconds)
|
||||||
|
else -> ExpiryMode.NONE
|
||||||
|
}
|
||||||
|
} ?: ExpiryMode.NONE
|
||||||
|
|
||||||
fun MessageReceiver.handleVisibleMessage(
|
fun MessageReceiver.handleVisibleMessage(
|
||||||
message: VisibleMessage,
|
message: VisibleMessage,
|
||||||
proto: SignalServiceProtos.Content,
|
proto: SignalServiceProtos.Content,
|
||||||
@ -583,8 +583,8 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
|
|||||||
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
|
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
|
||||||
val members = kind.members.map { it.toByteArray().toHexString() }
|
val members = kind.members.map { it.toByteArray().toHexString() }
|
||||||
val admins = kind.admins.map { it.toByteArray().toHexString() }
|
val admins = kind.admins.map { it.toByteArray().toHexString() }
|
||||||
val expireTimer = kind.expirationTimer
|
val expirationTimer = kind.expirationTimer
|
||||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!, expireTimer)
|
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!, expirationTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long, expireTimer: Int) {
|
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long, expireTimer: Int) {
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
package org.session.libsession.utilities
|
|
||||||
|
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
|
||||||
import org.session.libsignal.protos.SignalServiceProtos
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
fun ExpiryMode?.typeRadioIndex(): Int {
|
|
||||||
return when (this) {
|
|
||||||
is ExpiryMode.AfterRead -> SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_READ_VALUE
|
|
||||||
is ExpiryMode.AfterSend -> SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_SEND_VALUE
|
|
||||||
is ExpiryMode.Legacy -> SignalServiceProtos.Content.ExpirationType.UNKNOWN_VALUE
|
|
||||||
else -> -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun SignalServiceProtos.Content.ExpirationType?.expiryMode(durationSeconds: Long): ExpiryMode? = when (this) {
|
|
||||||
null -> null
|
|
||||||
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(durationSeconds)
|
|
||||||
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_SEND -> ExpiryMode.AfterSend(durationSeconds)
|
|
||||||
SignalServiceProtos.Content.ExpirationType.UNKNOWN -> ExpiryMode.Legacy(durationSeconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Int.expiryType(): KClass<out ExpiryMode>? {
|
|
||||||
if (this == -1) return null
|
|
||||||
return when (this) {
|
|
||||||
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_READ_VALUE -> ExpiryMode.AfterSend::class
|
|
||||||
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_SEND_VALUE -> ExpiryMode.AfterRead::class
|
|
||||||
SignalServiceProtos.Content.ExpirationType.UNKNOWN_VALUE -> ExpiryMode.Legacy::class
|
|
||||||
else -> ExpiryMode.NONE::class
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user