diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java index 9d9c350218..cee1887b40 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java @@ -78,13 +78,11 @@ public class FromTextView extends SimpleEmojiTextView { setText(builder); - if (recipient.isBlocked()) setCompoundDrawablesRelativeWithIntrinsicBounds(getBlocked(), null, null, null); - else if (RemoteConfig.getInlinePinnedChats() && isPinned) setCompoundDrawablesRelativeWithIntrinsicBounds(getPinned(), null, null, null); - else setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0); - } - - private Drawable getBlocked() { - return getDrawable(R.drawable.symbol_block_16, R.color.signal_icon_tint_secondary); + if (RemoteConfig.getInlinePinnedChats() && isPinned) { + setCompoundDrawablesRelativeWithIntrinsicBounds(getPinned(), null, null, null); + } else { + setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0); + } } private Drawable getMuted() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java index 60b56d15af..9b734b5f21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -503,7 +503,8 @@ public final class ConversationListItem extends ConstraintLayout implements Bind thread.isOutgoingAudioCall() || thread.isOutgoingVideoCall() || thread.isVerificationStatusChange() || - thread.isScheduledMessage()) + thread.isScheduledMessage() || + thread.getRecipient().isBlocked()) { deliveryStatusIndicator.setNone(); alertView.setNone(); @@ -586,6 +587,8 @@ public final class ConversationListItem extends ConstraintLayout implements Bind } else { return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_request), defaultTint); } + } else if (thread.getRecipient().isBlocked()) { + return emphasisAdded(context, context.getString(R.string.ThreadRecord_blocked), R.drawable.symbol_block_16, defaultTint); } else if (MessageTypes.isGroupUpdate(thread.getType())) { if (thread.getRecipient().isPushV2Group()) { if (thread.getMessageExtras() != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index f794096632..72309ccd7e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -420,6 +420,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat $TYPE & ${MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT} = 0 AND $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM} AND $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED} AND + $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_BLOCKED} AND + $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_UNBLOCKED} AND $TYPE NOT IN ( ${MessageTypes.PROFILE_CHANGE_TYPE}, ${MessageTypes.GV1_MIGRATION_TYPE}, @@ -1911,7 +1913,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat $TYPE != ${MessageTypes.RELEASE_CHANNEL_DONATION_REQUEST_TYPE} AND $TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS} AND $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM} AND - $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED} + $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED} AND + $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_BLOCKED} AND + $TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_UNBLOCKED} ) """ @@ -2556,6 +2560,18 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat sentTimeMillis = timestamp, expiresIn = expiresIn ) + } else if (MessageTypes.isBlocked(outboxType)) { + OutgoingMessage.blockedMessage( + threadRecipient = threadRecipient, + sentTimeMillis = timestamp, + expiresIn = expiresIn + ) + } else if (MessageTypes.isUnblocked(outboxType)) { + OutgoingMessage.unblockedMessage( + threadRecipient = threadRecipient, + sentTimeMillis = timestamp, + expiresIn = expiresIn + ) } else { val giftBadge: GiftBadge? = if (body != null && MessageTypes.isGiftBadge(outboxType)) { GiftBadge.ADAPTER.decode(Base64.decode(body)) @@ -2727,6 +2743,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat !MessageTypes.isReportedSpam(type) && !MessageTypes.isMessageRequestAccepted(type) && !MessageTypes.isExpirationTimerUpdate(type) && + !MessageTypes.isBlocked(type) && + !MessageTypes.isUnblocked(type) && !retrieved.storyType.isStory && isNotStoryGroupReply && !silent @@ -2860,8 +2878,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat fun insertMessageOutbox( message: OutgoingMessage, threadId: Long, - forceSms: Boolean, - insertListener: InsertListener? + forceSms: Boolean = false, + insertListener: InsertListener? = null ): Long { return insertMessageOutbox( message = message, @@ -2904,7 +2922,16 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat } if (message.isGroup) { - if (message.isV2Group) { + if (message.isBlocked) { + type = type or MessageTypes.GROUP_V2_BIT or MessageTypes.SPECIAL_TYPE_BLOCKED + hasSpecialType = true + } else if (message.isUnblocked) { + if (hasSpecialType) { + throw MmsException("Cannot insert message with multiple special types.") + } + type = type or MessageTypes.GROUP_V2_BIT or MessageTypes.SPECIAL_TYPE_UNBLOCKED + hasSpecialType = true + } else if (message.isV2Group) { type = type or (MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT) if (message.isJustAGroupLeave) { @@ -2926,6 +2953,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat } if (message.isStoryReaction) { + if (hasSpecialType) { + throw MmsException("Cannot insert message with multiple special types.") + } type = type or MessageTypes.SPECIAL_TYPE_STORY_REACTION hasSpecialType = true } @@ -2978,6 +3008,22 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat hasSpecialType = true } + if (message.isBlocked && !message.isGroup) { + if (hasSpecialType) { + throw MmsException("Cannot insert message with multiple special types.") + } + type = type or MessageTypes.SPECIAL_TYPE_BLOCKED + hasSpecialType = true + } + + if (message.isUnblocked && !message.isGroup) { + if (hasSpecialType) { + throw MmsException("Cannot insert message with multiple special types.") + } + type = type or MessageTypes.SPECIAL_TYPE_UNBLOCKED + hasSpecialType = true + } + val earlyDeliveryReceipts: Map = earlyDeliveryReceiptCache.remove(message.sentTimeMillis) if (earlyDeliveryReceipts.isNotEmpty()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index affb1d8a80..0c235310cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -248,4 +248,12 @@ public abstract class DisplayRecord { public boolean isMessageRequestAccepted() { return MessageTypes.isMessageRequestAccepted(type); } + + public boolean isBlocked() { + return MessageTypes.isBlocked(type); + } + + public boolean isUnblocked() { + return MessageTypes.isUnblocked(type); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 9018082f46..715736960c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -283,6 +283,10 @@ public abstract class MessageRecord extends DisplayRecord { return staticUpdateDescription(context.getString(R.string.MessageRecord_reported_as_spam), R.drawable.symbol_spam_16); } else if (isMessageRequestAccepted()) { return staticUpdateDescription(context.getString(R.string.MessageRecord_you_accepted_the_message_request), R.drawable.symbol_thread_16); + } else if (isBlocked()) { + return staticUpdateDescription(context.getString(isGroupV2() ? R.string.MessageRecord_you_blocked_this_group : R.string.MessageRecord_you_blocked_this_person), R.drawable.symbol_block_16); + } else if (isUnblocked()) { + return staticUpdateDescription(context.getString(isGroupV2() ? R.string.MessageRecord_you_unblocked_this_group : R.string.MessageRecord_you_unblocked_this_person) , R.drawable.symbol_thread_16); } return null; @@ -688,7 +692,8 @@ public abstract class MessageRecord extends DisplayRecord { isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType() || isChangeNumber() || isReleaseChannelDonationRequest() || isThreadMergeEventType() || isSmsExportType() || isSessionSwitchoverEventType() || - isPaymentsRequestToActivate() || isPaymentsActivated() || isReportedSpam() || isMessageRequestAccepted(); + isPaymentsRequestToActivate() || isPaymentsActivated() || isReportedSpam() || isMessageRequestAccepted() || + isBlocked() || isUnblocked(); } public boolean isMediaPending() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index 812480f443..c9cbbafaa1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -317,6 +317,8 @@ public class MmsMessageRecord extends MessageRecord { (type & MessageTypes.KEY_EXCHANGE_MASK) == 0 && !isReportedSpam() && !isMessageRequestAccepted() && + !isBlocked() && + !isUnblocked() && storyType == StoryType.NONE && getDateSent() > 0 && (parentStoryId == null || parentStoryId.isDirectReply()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index e472fea13f..efd5313bd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -1141,14 +1141,20 @@ object SyncMessageProcessor { when (response.type) { MessageRequestResponse.Type.ACCEPT -> { + val wasBlocked = recipient.isBlocked SignalDatabase.recipients.setProfileSharing(recipient.id, true) SignalDatabase.recipients.setBlocked(recipient.id, false) - SignalDatabase.messages.insertMessageOutbox( - OutgoingMessage.messageRequestAcceptMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())), - threadId, - false, - null - ) + if (wasBlocked) { + SignalDatabase.messages.insertMessageOutbox( + message = OutgoingMessage.unblockedMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())), + threadId = threadId + ) + } else { + SignalDatabase.messages.insertMessageOutbox( + message = OutgoingMessage.messageRequestAcceptMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())), + threadId = threadId + ) + } } MessageRequestResponse.Type.DELETE -> { SignalDatabase.recipients.setProfileSharing(recipient.id, false) @@ -1159,6 +1165,10 @@ object SyncMessageProcessor { MessageRequestResponse.Type.BLOCK -> { SignalDatabase.recipients.setBlocked(recipient.id, true) SignalDatabase.recipients.setProfileSharing(recipient.id, false) + SignalDatabase.messages.insertMessageOutbox( + message = OutgoingMessage.blockedMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())), + threadId = threadId + ) } MessageRequestResponse.Type.BLOCK_AND_DELETE -> { SignalDatabase.recipients.setBlocked(recipient.id, true) @@ -1169,20 +1179,20 @@ object SyncMessageProcessor { } MessageRequestResponse.Type.SPAM -> { SignalDatabase.messages.insertMessageOutbox( - OutgoingMessage.reportSpamMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())), - threadId, - false, - null + message = OutgoingMessage.reportSpamMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())), + threadId = threadId ) } MessageRequestResponse.Type.BLOCK_AND_SPAM -> { SignalDatabase.recipients.setBlocked(recipient.id, true) SignalDatabase.recipients.setProfileSharing(recipient.id, false) SignalDatabase.messages.insertMessageOutbox( - OutgoingMessage.reportSpamMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())), - threadId, - false, - null + message = OutgoingMessage.reportSpamMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())), + threadId = threadId + ) + SignalDatabase.messages.insertMessageOutbox( + message = OutgoingMessage.blockedMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())), + threadId = threadId ) } else -> warn("Got an unknown response type! Skipping") diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMessage.kt index f28afb8a7c..e9de72ed66 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMessage.kt @@ -55,6 +55,8 @@ data class OutgoingMessage( val messageToEdit: Long = 0, val isReportSpam: Boolean = false, val isMessageRequestAccept: Boolean = false, + val isBlocked: Boolean = false, + val isUnblocked: Boolean = false, val messageExtras: MessageExtras? = null ) { @@ -435,6 +437,38 @@ data class OutgoingMessage( ) } + /** + * Message for when you block someone + */ + @JvmStatic + fun blockedMessage(threadRecipient: Recipient, sentTimeMillis: Long, expiresIn: Long): OutgoingMessage { + return OutgoingMessage( + threadRecipient = threadRecipient, + sentTimeMillis = sentTimeMillis, + expiresIn = expiresIn, + isGroup = threadRecipient.isPushV2Group, + isBlocked = true, + isUrgent = false, + isSecure = true + ) + } + + /** + * Message for when you unblock someone + */ + @JvmStatic + fun unblockedMessage(threadRecipient: Recipient, sentTimeMillis: Long, expiresIn: Long): OutgoingMessage { + return OutgoingMessage( + threadRecipient = threadRecipient, + sentTimeMillis = sentTimeMillis, + expiresIn = expiresIn, + isGroup = threadRecipient.isPushV2Group, + isUnblocked = true, + isUrgent = false, + isSecure = true + ) + } + @JvmStatic fun buildMessage(slideDeck: SlideDeck, message: String): String { return if (message.isNotEmpty() && slideDeck.body.isNotEmpty()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java index d0b7e57e3b..6997dd232c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java @@ -24,10 +24,10 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob; import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMessage; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.storage.StorageSyncHelper; -import org.thoughtcrime.securesms.util.ExpirationTimerUtil; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; @@ -36,6 +36,7 @@ import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.concurrent.TimeUnit; public class RecipientUtil { @@ -172,6 +173,7 @@ public class RecipientUtil { } SignalDatabase.recipients().setBlocked(recipient.getId(), true); + insertBlockedUpdate(recipient, SignalDatabase.threads().getOrCreateThreadIdFor(recipient)); if (recipient.isSystemContact() || recipient.isProfileSharing() || isProfileSharedViaGroup(recipient)) { SignalDatabase.recipients().setProfileSharing(recipient.getId(), false); @@ -194,10 +196,37 @@ public class RecipientUtil { SignalDatabase.recipients().setBlocked(recipient.getId(), false); SignalDatabase.recipients().setProfileSharing(recipient.getId(), true); + insertUnblockedUpdate(recipient, SignalDatabase.threads().getOrCreateThreadIdFor(recipient)); AppDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob()); StorageSyncHelper.scheduleSyncForDataChange(); } + private static void insertBlockedUpdate(@NonNull Recipient recipient, long threadId) { + try { + SignalDatabase.messages().insertMessageOutbox( + OutgoingMessage.blockedMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds())), + threadId, + false, + null + ); + } catch (MmsException e) { + Log.w(TAG, "Unable to insert blocked message", e); + } + } + + private static void insertUnblockedUpdate(@NonNull Recipient recipient, long threadId) { + try { + SignalDatabase.messages().insertMessageOutbox( + OutgoingMessage.unblockedMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds())), + threadId, + false, + null + ); + } catch (MmsException e) { + Log.w(TAG, "Unable to insert unblocked message", e); + } + } + @WorkerThread public static Recipient.HiddenState getRecipientHiddenState(long threadId) { if (threadId < 0) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a31bcea27a..e6f33e8ce4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2082,6 +2082,14 @@ Reported as spam You accepted the message request + + You blocked this person + + You unblocked this person + + You blocked this group + + You unblocked this group Accept @@ -2849,6 +2857,8 @@ Your message history has been merged %1$s belongs to %2$s + + Blocked Signal update diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt b/app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt index ae5adda15d..87645b4d76 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt @@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.database.MessageTypes.RELEASE_CHANNEL_DONATION import org.thoughtcrime.securesms.database.MessageTypes.SECURE_MESSAGE_BIT import org.thoughtcrime.securesms.database.MessageTypes.SMS_EXPORT_TYPE import org.thoughtcrime.securesms.database.MessageTypes.SPECIAL_TYPES_MASK +import org.thoughtcrime.securesms.database.MessageTypes.SPECIAL_TYPE_BLOCKED import org.thoughtcrime.securesms.database.MessageTypes.SPECIAL_TYPE_GIFT_BADGE import org.thoughtcrime.securesms.database.MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED import org.thoughtcrime.securesms.database.MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED @@ -58,6 +59,7 @@ import org.thoughtcrime.securesms.database.MessageTypes.SPECIAL_TYPE_PAYMENTS_AC import org.thoughtcrime.securesms.database.MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION import org.thoughtcrime.securesms.database.MessageTypes.SPECIAL_TYPE_REPORTED_SPAM import org.thoughtcrime.securesms.database.MessageTypes.SPECIAL_TYPE_STORY_REACTION +import org.thoughtcrime.securesms.database.MessageTypes.SPECIAL_TYPE_UNBLOCKED import org.thoughtcrime.securesms.database.MessageTypes.THREAD_MERGE_TYPE import org.thoughtcrime.securesms.database.MessageTypes.UNSUPPORTED_MESSAGE_TYPE @@ -135,6 +137,8 @@ object MessageBitmaskColumnTransformer : ColumnTransformer { isPaymentsActivated:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_PAYMENTS_ACTIVATED} isReportSpam:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_REPORTED_SPAM} isMessageRequestAccepted:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED} + isBlockedType:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_BLOCKED} + isUnblockedType:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_UNBLOCKED} """.trimIndent() return "$type

" + describe.replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "
")