feat: Add conversation filtering for message requests (#830)

* feat: Message requests

* Apply contact sync message

* Filter based on message requests toggle

* Add message requests screen

* Implement message requests screen

* Handle message request buttons

* Handle approval syncing

* Display message request response

* Display pending message request

* Display pending message request

* Add approval migrations

* Send message request response

* Fix conversation filters

* Add approval migration

* Handle message request response

* Update message request response proto

* Update message request response handling

* Refresh message requests

* Show message request banner on new message request

* Message request item layout tweaks

* Fix latest unapproved conversation query

* Handle sent message request responses on restore

* QA feedback tweaks

* Remove send limit on message requests

* Config message handling tweaks

* Reverse conversation upon message request approval

* Remove read receipts, delete declined conversations

* Fix contact filtering in config messages

* Fix message request order and handle deletion

* Fix message request snippet on home screen

* Refresh message request list after decline or clearing all

* Fix message request reversal

* Fix message request notifications

* Disable media buttons for message requests

* Hide message request banner after reading

* Refresh message request banner
This commit is contained in:
ceokot
2022-03-04 08:46:39 +02:00
committed by GitHub
parent 55aa266769
commit 206505abe8
56 changed files with 2427 additions and 205 deletions

View File

@@ -8,6 +8,7 @@ import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupV2
@@ -156,4 +157,5 @@ interface StorageProtocol {
*/
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long?
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
fun insertMessageRequestResponse(response: MessageRequestResponse)
}

View File

@@ -60,19 +60,22 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
}
}
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) {
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?, var isApproved: Boolean?, var isBlocked: Boolean?, var didApproveMe: Boolean?) {
internal constructor() : this("", "", null, null)
internal constructor() : this("", "", null, null, null, null, null)
companion object {
fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.Contact): Contact? {
if (!proto.hasName() || !proto.hasProfileKey()) return null
if (!proto.hasName()) return null
val publicKey = proto.publicKey.toByteArray().toHexString()
val name = proto.name
val profilePicture = if (proto.hasProfilePicture()) proto.profilePicture else null
val profileKey = if (proto.hasProfileKey()) proto.profileKey.toByteArray() else null
return Contact(publicKey, name, profilePicture, profileKey)
val isApproved = if (proto.hasIsApproved()) proto.isApproved else null
val isBlocked = if (proto.hasIsBlocked()) proto.isBlocked else null
val didApproveMe = if (proto.hasDidApproveMe()) proto.didApproveMe else null
return Contact(publicKey, name, profilePicture, profileKey, isApproved, isBlocked, didApproveMe)
}
}
@@ -92,6 +95,18 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
if (profileKey != null) {
result.profileKey = ByteString.copyFrom(profileKey)
}
val isApproved = isApproved
if (isApproved != null) {
result.isApproved = isApproved
}
val isBlocked = isBlocked
if (isBlocked != null) {
result.isBlocked = isBlocked
}
val didApproveMe = didApproveMe
if (didApproveMe != null) {
result.didApproveMe = didApproveMe
}
return result.build()
}
}

View File

@@ -0,0 +1,33 @@
package org.session.libsession.messaging.messages.control
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Log
class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() {
override val isSelfSendValid: Boolean = true
override fun toProto(): SignalServiceProtos.Content? {
val messageRequestResponseProto = SignalServiceProtos.MessageRequestResponse.newBuilder()
.setIsApproved(isApproved)
return try {
SignalServiceProtos.Content.newBuilder()
.setMessageRequestResponse(messageRequestResponseProto.build())
.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct message request response proto from: $this")
null
}
}
companion object {
const val TAG = "MessageRequestResponse"
fun fromProto(proto: SignalServiceProtos.Content): MessageRequestResponse? {
val messageRequestResponseProto = if (proto.hasMessageRequestResponse()) proto.messageRequestResponse else return null
val isApproved = messageRequestResponseProto.isApproved
return MessageRequestResponse(isApproved)
}
}
}

View File

@@ -28,6 +28,7 @@ public class IncomingMediaMessage {
private final long expiresIn;
private final boolean expirationUpdate;
private final boolean unidentified;
private final boolean messageRequestResponse;
private final DataExtractionNotificationInfoMessage dataExtractionNotification;
private final QuoteModel quote;
@@ -42,6 +43,7 @@ public class IncomingMediaMessage {
long expiresIn,
boolean expirationUpdate,
boolean unidentified,
boolean messageRequestResponse,
Optional<String> body,
Optional<SignalServiceGroup> group,
Optional<List<SignalServiceAttachment>> attachments,
@@ -60,6 +62,7 @@ public class IncomingMediaMessage {
this.dataExtractionNotification = dataExtractionNotification.orNull();
this.quote = quote.orNull();
this.unidentified = unidentified;
this.messageRequestResponse = messageRequestResponse;
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get()));
else this.groupId = null;
@@ -78,7 +81,7 @@ public class IncomingMediaMessage {
Optional<List<LinkPreview>> linkPreviews)
{
return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, false,
false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews, Optional.absent());
false, false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews, Optional.absent());
}
public int getSubscriptionId() {
@@ -150,4 +153,8 @@ public class IncomingMediaMessage {
public boolean isUnidentified() {
return unidentified;
}
public boolean isMessageRequestResponse() {
return messageRequestResponse;
}
}

View File

@@ -95,6 +95,7 @@ object MessageReceiver {
ExpirationTimerUpdate.fromProto(proto) ?:
ConfigurationMessage.fromProto(proto) ?:
UnsendRequest.fromProto(proto) ?:
MessageRequestResponse.fromProto(proto) ?:
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
// Ignore self send if needed
if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend

View File

@@ -5,7 +5,14 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.*
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.DataExtractionNotification
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.ReadReceipt
import org.session.libsession.messaging.messages.control.TypingIndicator
import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
@@ -15,7 +22,12 @@ import org.session.libsession.messaging.sending_receiving.notifications.PushNoti
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.*
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey
@@ -28,8 +40,7 @@ import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.toHexString
import java.security.MessageDigest
import java.util.*
import kotlin.collections.ArrayList
import java.util.LinkedList
internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
val context = MessagingModuleConfiguration.shared.context
@@ -46,6 +57,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
is DataExtractionNotification -> handleDataExtractionNotification(message)
is ConfigurationMessage -> handleConfigurationMessage(message)
is UnsendRequest -> handleUnsendRequest(message)
is MessageRequestResponse -> handleMessageRequestResponse(message)
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
}
}
@@ -160,6 +172,10 @@ fun MessageReceiver.handleUnsendRequest(message: UnsendRequest) {
SSKEnvironment.shared.notificationManager.updateNotification(context)
}
}
fun handleMessageRequestResponse(message: MessageRequestResponse) {
MessagingModuleConfiguration.shared.storage.insertMessageRequestResponse(message)
}
//endregion
// region Visible Messages
@@ -177,10 +193,10 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
throw MessageReceiver.Error.NoThread
}
// Update profile if needed
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
val profile = message.profile
if (profile != null && userPublicKey != message.sender) {
val profileManager = SSKEnvironment.shared.profileManager
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
val name = profile.displayName!!
if (name.isNotEmpty()) {
profileManager.setName(context, recipient, name)
@@ -269,6 +285,8 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup
private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) {
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return
val recipient = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(message.sender!!), false)
if (!recipient.isApproved) return
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
val members = kind.members.map { it.toByteArray().toHexString() }
val admins = kind.admins.map { it.toByteArray().toHexString() }

View File

@@ -151,6 +151,8 @@ interface TextSecurePreferences {
fun setLastOpenDate()
fun hasSeenLinkPreviewSuggestionDialog(): Boolean
fun setHasSeenLinkPreviewSuggestionDialog()
fun hasHiddenMessageRequests(): Boolean
fun setHasHiddenMessageRequests()
fun clearAll()
companion object {
@@ -227,6 +229,7 @@ interface TextSecurePreferences {
const val CONFIGURATION_SYNCED = "pref_configuration_synced"
const val LAST_PROFILE_UPDATE_TIME = "pref_last_profile_update_time"
const val LAST_OPEN_DATE = "pref_last_open_date"
const val HAS_HIDDEN_MESSAGE_REQUESTS = "pref_message_requests_hidden"
@JvmStatic
fun getLastConfigurationSyncTime(context: Context): Long {
@@ -870,6 +873,16 @@ interface TextSecurePreferences {
setBooleanPreference(context, "has_seen_link_preview_suggestion_dialog", true)
}
@JvmStatic
fun hasHiddenMessageRequests(context: Context): Boolean {
return getBooleanPreference(context, HAS_HIDDEN_MESSAGE_REQUESTS, false)
}
@JvmStatic
fun removeHasHiddenMessageRequests(context: Context) {
removePreference(context, HAS_HIDDEN_MESSAGE_REQUESTS)
}
@JvmStatic
fun clearAll(context: Context) {
getDefaultSharedPreferences(context).edit().clear().commit()
@@ -1426,6 +1439,14 @@ class AppTextSecurePreferences @Inject constructor(
setBooleanPreference("has_seen_link_preview_suggestion_dialog", true)
}
override fun hasHiddenMessageRequests(): Boolean {
return getBooleanPreference(TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS, false)
}
override fun setHasHiddenMessageRequests() {
setBooleanPreference(TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS, true)
}
override fun clearAll() {
getDefaultSharedPreferences(context).edit().clear().commit()
}

View File

@@ -81,6 +81,8 @@ public class Recipient implements RecipientModifiedListener {
public long mutedUntil = 0;
public int notifyType = 0;
private boolean blocked = false;
private boolean approved = false;
private boolean approvedMe = false;
private VibrateState messageVibrate = VibrateState.DEFAULT;
private VibrateState callVibrate = VibrateState.DEFAULT;
private int expireMessages = 0;
@@ -141,6 +143,8 @@ public class Recipient implements RecipientModifiedListener {
this.callRingtone = stale.callRingtone;
this.mutedUntil = stale.mutedUntil;
this.blocked = stale.blocked;
this.approved = stale.approved;
this.approvedMe = stale.approvedMe;
this.messageVibrate = stale.messageVibrate;
this.callVibrate = stale.callVibrate;
this.expireMessages = stale.expireMessages;
@@ -169,6 +173,8 @@ public class Recipient implements RecipientModifiedListener {
this.callRingtone = details.get().callRingtone;
this.mutedUntil = details.get().mutedUntil;
this.blocked = details.get().blocked;
this.approved = details.get().approved;
this.approvedMe = details.get().approvedMe;
this.messageVibrate = details.get().messageVibrateState;
this.callVibrate = details.get().callVibrateState;
this.expireMessages = details.get().expireMessages;
@@ -570,6 +576,30 @@ public class Recipient implements RecipientModifiedListener {
notifyListeners();
}
public synchronized boolean isApproved() {
return approved;
}
public void setApproved(boolean approved) {
synchronized (this) {
this.approved = approved;
}
notifyListeners();
}
public synchronized boolean hasApprovedMe() {
return approvedMe;
}
public void setHasApprovedMe(boolean approvedMe) {
synchronized (this) {
this.approvedMe = approvedMe;
}
notifyListeners();
}
public synchronized VibrateState getMessageVibrate() {
return messageVibrate;
}
@@ -779,6 +809,8 @@ public class Recipient implements RecipientModifiedListener {
public static class RecipientSettings {
private final boolean blocked;
private final boolean approved;
private final boolean approvedMe;
private final long muteUntil;
private final int notifyType;
private final VibrateState messageVibrateState;
@@ -801,7 +833,7 @@ public class Recipient implements RecipientModifiedListener {
private final UnidentifiedAccessMode unidentifiedAccessMode;
private final boolean forceSmsSelection;
public RecipientSettings(boolean blocked, long muteUntil,
public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil,
int notifyType,
@NonNull VibrateState messageVibrateState,
@NonNull VibrateState callVibrateState,
@@ -824,6 +856,8 @@ public class Recipient implements RecipientModifiedListener {
boolean forceSmsSelection)
{
this.blocked = blocked;
this.approved = approved;
this.approvedMe = approvedMe;
this.muteUntil = muteUntil;
this.notifyType = notifyType;
this.messageVibrateState = messageVibrateState;
@@ -855,6 +889,14 @@ public class Recipient implements RecipientModifiedListener {
return blocked;
}
public boolean isApproved() {
return approved;
}
public boolean hasApprovedMe() {
return approvedMe;
}
public long getMuteUntil() {
return muteUntil;
}

View File

@@ -171,6 +171,8 @@ class RecipientProvider {
@Nullable final VibrateState messageVibrateState;
@Nullable final VibrateState callVibrateState;
final boolean blocked;
final boolean approved;
final boolean approvedMe;
final int expireMessages;
@NonNull final List<Recipient> participants;
@Nullable final String profileName;
@@ -201,6 +203,8 @@ class RecipientProvider {
this.messageVibrateState = settings != null ? settings.getMessageVibrateState() : null;
this.callVibrateState = settings != null ? settings.getCallVibrateState() : null;
this.blocked = settings != null && settings.isBlocked();
this.approved = settings != null && settings.isApproved();
this.approvedMe = settings != null && settings.hasApprovedMe();
this.expireMessages = settings != null ? settings.getExpireMessages() : 0;
this.participants = participants == null ? new LinkedList<>() : participants;
this.profileName = settings != null ? settings.getProfileName() : null;