diff --git a/CHANGES.md b/CHANGES.md index e9789c1414..d2ba5babec 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,9 +8,11 @@ Improvements 🙌: - Split network request `/keys/query` into smaller requests (250 users max) (#2925) - Crypto improvement | Bulk send NO_OLM withheld code - Display the room shield in all room setting screens + - Improve message with Emoji only detection (#3017) Bugfix 🐛: - - + - Fix bad theme change for the MainActivity + - Handle encrypted reactions (#2509) Translations 🗣: - diff --git a/docs/notifications.md b/docs/notifications.md index 63bf593d0d..a00fef8fae 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -2,7 +2,7 @@ This document aims to describe how Element android displays notifications to the # Table of Contents 1. [Prerequisites Knowledge](#prerequisites-knowledge) - * [How does a matrix client gets a message from a Home Server?](#how-does-a-matrix-client-gets-a-message-from-a-home-server) + * [How does a matrix client get a message from a Home Server?](#how-does-a-matrix-client-get-a-message-from-a-home-server) * [How does a mobile app receives push notification?](#how-does-a-mobile-app-receives-push-notification) * [Push VS Notification](#push-vs-notification) * [Push in the matrix federated world](#push-in-the-matrix-federated-world) @@ -22,7 +22,7 @@ First let's start with some prerequisite knowledge # Prerequisites Knowledge -## How does a matrix client gets a message from a Home Server? +## How does a matrix client get a message from a Home Server? In order to get messages from a home server, a matrix client need to perform a ``sync`` operation. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 60440c6359..c7e09e5954 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -53,8 +53,9 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import timber.log.Timber import javax.inject.Inject -internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String) - : EventInsertLiveProcessor { +internal class EventRelationsAggregationProcessor @Inject constructor( + @UserId private val userId: String +) : EventInsertLiveProcessor { private val allowedTypes = listOf( EventType.MESSAGE, @@ -87,12 +88,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr EventType.REACTION -> { // we got a reaction!! Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") - handleReaction(event, roomId, realm, userId, isLocalEcho) + handleReaction(realm, event, roomId, isLocalEcho) } EventType.MESSAGE -> { if (event.unsignedData?.relations?.annotations != null) { - Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") - handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) + Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}") + handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations) EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst() ?.let { @@ -108,7 +109,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr handleReplace(realm, event, content, roomId, isLocalEcho) } else if (content?.relatesTo?.type == RelationType.RESPONSE) { Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") - handleResponse(realm, userId, event, content, roomId, isLocalEcho) + handleResponse(realm, event, content, roomId, isLocalEcho) } } @@ -122,7 +123,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr Timber.v("## SAS REF in room $roomId for event ${event.eventId}") event.content.toModel()?.relatesTo?.let { if (it.type == RelationType.REFERENCE && it.eventId != null) { - handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId) + handleVerification(realm, event, roomId, isLocalEcho, it.eventId) } } } @@ -140,7 +141,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) } else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) { Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") - handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + handleResponse(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) } } } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { @@ -154,10 +155,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr EventType.KEY_VERIFICATION_KEY -> { Timber.v("## SAS REF in room $roomId for event ${event.eventId}") encryptedEventContent.relatesTo.eventId?.let { - handleVerification(realm, event, roomId, isLocalEcho, it, userId) + handleVerification(realm, event, roomId, isLocalEcho, it) } } } + } else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) { + // Reaction + if (event.getClearType() == EventType.REACTION) { + // we got a reaction!! + Timber.v("###REACTION e2e in room $roomId , reaction eventID ${event.eventId}") + handleReaction(realm, event, roomId, isLocalEcho) + } } } EventType.REDACTION -> { @@ -172,11 +180,11 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr // was this event a m.replace val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { - handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) + handleRedactionOfReplace(realm, eventToPrune, contentModel.relatesTo!!.eventId!!) } } EventType.REACTION -> { - handleReactionRedact(eventToPrune, realm, userId) + handleReactionRedact(realm, eventToPrune) } } } @@ -267,7 +275,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr } private fun handleResponse(realm: Realm, - userId: String, event: Event, content: MessageContent, roomId: String, @@ -354,7 +361,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr existingPollSummary.aggregatedContent = ContentMapper.map(sumModel.toContent()) } - private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { + private fun handleInitialAggregatedRelations(realm: Realm, + event: Event, + roomId: String, + aggregation: AggregatedAnnotation) { if (SHOULD_HANDLE_SERVER_AGREGGATION) { aggregation.chunk?.forEach { if (it.type == EventType.REACTION) { @@ -376,7 +386,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr } } - private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) { + private fun handleReaction(realm: Realm, + event: Event, + roomId: String, + isLocalEcho: Boolean) { val content = event.content.toModel() if (content == null) { Timber.e("Malformed reaction content ${event.content}") @@ -441,7 +454,9 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr /** * Called when an event is deleted */ - private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) { + private fun handleRedactionOfReplace(realm: Realm, + redacted: EventEntity, + relatedEventId: String) { Timber.d("Handle redaction of m.replace") val eventSummary = EventAnnotationsSummaryEntity.where(realm, redacted.roomId, relatedEventId).findFirst() if (eventSummary == null) { @@ -457,7 +472,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr sourceToDiscard.deleteFromRealm() } - private fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) { + private fun handleReactionRedact(realm: Realm, + eventToPrune: EventEntity) { Timber.v("REDACTION of reaction ${eventToPrune.eventId}") // delete a reaction, need to update the annotation summary if any val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() ?: return @@ -494,7 +510,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr } } - private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) { + private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String) { val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId) val verifSummary = eventSummary.referencesSummaryEntity diff --git a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt index b3b9a39f30..66907ded10 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Emoji.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Emoji.kt @@ -16,62 +16,7 @@ package im.vector.app.core.utils -import java.util.regex.Pattern - -private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" + - "|[\uD83E\uDD00-\uD83E\uDDFF]" + - "|[\uD83D\uDE00-\uD83D\uDE4F]" + - "|[\uD83D\uDE80-\uD83D\uDEFF]" + - "|[\u2600-\u26FF]\uFE0F?" + - "|[\u2700-\u27BF]\uFE0F?" + - "|\u24C2\uFE0F?" + - "|[\uD83C\uDDE6-\uD83C\uDDFF]{1,2}" + - "|[\uD83C\uDD70\uD83C\uDD71\uD83C\uDD7E\uD83C\uDD7F\uD83C\uDD8E\uD83C\uDD91-\uD83C\uDD9A]\uFE0F?" + - "|[\u0023\u002A\u0030-\u0039]\uFE0F?\u20E3" + - "|[\u2194-\u2199\u21A9-\u21AA]\uFE0F?" + - "|[\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55]\uFE0F?" + - "|[\u2934\u2935]\uFE0F?" + - "|[\u3030\u303D]\uFE0F?" + - "|[\u3297\u3299]\uFE0F?" + - "|[\uD83C\uDE01\uD83C\uDE02\uD83C\uDE1A\uD83C\uDE2F\uD83C\uDE32-\uD83C\uDE3A\uD83C\uDE50\uD83C\uDE51]\uFE0F?" + - "|[\u203C\u2049]\uFE0F?" + - "|[\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE]\uFE0F?" + - "|[\u00A9\u00AE]\uFE0F?" + - "|[\u2122\u2139]\uFE0F?" + - "|\uD83C\uDC04\uFE0F?" + - "|\uD83C\uDCCF\uFE0F?" + - "|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))") - -/* -// A hashset from all supported emoji -private var knownEmojiSet: HashSet? = null - -fun initKnownEmojiHashSet(context: Context, done: (() -> Unit)? = null) { - GlobalScope.launch { - context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input -> - val moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(EmojiData::class.java) - val inputAsString = input.bufferedReader().use { it.readText() } - val source = jsonAdapter.fromJson(inputAsString) - knownEmojiSet = HashSet().also { - source?.emojis?.mapTo(it) { (_, value) -> - value.emojiString() - } - } - done?.invoke() - } - } -} - -fun isSingleEmoji(string: String): Boolean { - if (knownEmojiSet == null) { - Timber.e("Known Emoji Hashset not initialized") - // use fallback regexp - return containsOnlyEmojis(string) - } - return knownEmojiSet?.contains(string) ?: false -} - */ +import com.vanniktech.emoji.EmojiUtils /** * Test if a string contains emojis. @@ -82,36 +27,8 @@ fun isSingleEmoji(string: String): Boolean { * @return true if the body contains only emojis */ fun containsOnlyEmojis(str: String?): Boolean { - var res = false - - if (str != null && str.isNotEmpty()) { - val matcher = emojisPattern.matcher(str) - - var start = -1 - var end = -1 - - while (matcher.find()) { - val nextStart = matcher.start() - - // first emoji position - if (start < 0) { - if (nextStart > 0) { - return false - } - } else { - // must not have a character between - if (nextStart != end) { - return false - } - } - start = nextStart - end = matcher.end() - } - - res = -1 != start && end == str.length - } - - return res + // Now rely on vanniktech library + return EmojiUtils.isOnlyEmojis(str) } /** diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index 143506d4df..e6c5abe20c 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -43,6 +43,7 @@ import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.signout.hard.SignedOutActivity import im.vector.app.features.signout.soft.SoftLogoutActivity +import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.ui.UiStateRepository import kotlinx.parcelize.Parcelize import kotlinx.coroutines.Dispatchers @@ -83,6 +84,8 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity override fun getBinding() = ActivityMainBinding.inflate(layoutInflater) + override fun getOtherThemes() = ActivityOtherThemes.Launcher + private lateinit var args: MainActivityArgs @Inject lateinit var notificationDrawerManager: NotificationDrawerManager diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt index 37d8bf9b46..ba2c923d8b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt @@ -30,10 +30,8 @@ import im.vector.app.R import im.vector.app.core.extensions.showPassword import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.databinding.FragmentSsssAccessFromPassphraseBinding import io.reactivex.android.schedulers.AndroidSchedulers - import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -59,8 +57,9 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor( key ) .toSpannable() - .colorizeMatchingText(pass, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) - .colorizeMatchingText(key, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + // TODO Restore coloration when we will have a FAQ to open with those terms + // .colorizeMatchingText(pass, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + // .colorizeMatchingText(key, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) views.ssssPassphraseEnterEdittext.editorActionEvents() .throttleFirst(300, TimeUnit.MILLISECONDS) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 48f3f0a460..fc526b5322 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -119,13 +119,18 @@ class IncomingVerificationRequestHandler @Inject constructor( Timber.v("## SAS verificationRequestCreated ${pr.transactionId}") // For incoming request we should prompt (if not in activity where this request apply) if (pr.isIncoming) { - val user = session?.getUser(pr.otherUserId) + val user = session?.getUser(pr.otherUserId)?.toMatrixItem() val name = user?.getBestName() ?: pr.otherUserId + val description = if (name == pr.otherUserId) { + name + } else { + "$name (${pr.otherUserId})" + } val alert = VerificationVectorAlert( uniqueIdForVerificationRequest(pr), context.getString(R.string.sas_incoming_request_notif_title), - "$name(${pr.otherUserId})", + description, R.drawable.ic_shield_black, shouldBeDisplayedIn = { activity -> if (activity is RoomDetailActivity) { @@ -136,7 +141,7 @@ class IncomingVerificationRequestHandler @Inject constructor( } ) .apply { - viewBinder = VerificationVectorAlert.ViewBinder(user?.toMatrixItem(), avatarRenderer.get()) + viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer.get()) contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { val roomId = pr.roomId diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index ccc8289e08..73f101d1f5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -64,7 +64,6 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_GUEST_ACCESS, EventType.STATE_ROOM_POWER_LEVELS, - EventType.REACTION, EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(event, highlight, callback) @@ -91,6 +90,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_MAC, + EventType.REACTION, EventType.CALL_CANDIDATES, EventType.CALL_REPLACES, EventType.CALL_SELECT_ANSWER, diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index 67ef0514f2..3228ffa6e1 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -26,6 +26,7 @@ import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.isAnimationDisabled import im.vector.app.features.pin.PinActivity +import im.vector.app.features.signout.hard.SignedOutActivity import im.vector.app.features.themes.ThemeUtils import timber.log.Timber import java.lang.ref.WeakReference @@ -294,6 +295,7 @@ class PopupAlertManager @Inject constructor() { private fun shouldBeDisplayedIn(alert: VectorAlert?, activity: Activity): Boolean { return alert != null && activity !is PinActivity + && activity !is SignedOutActivity && activity is VectorBaseActivity<*> && alert.shouldBeDisplayedIn.invoke(activity) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index bb7d041199..10e6dceebe 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -222,7 +222,6 @@ class RoomProfileController @Inject constructor( buildProfileAction( id = "devTools", title = stringProvider.getString(R.string.dev_tools_menu_name), - subtitle = roomSummary.roomId, dividerColor = dividerColor, divider = false, editable = true, diff --git a/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt b/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt index 847caeab4c..a1065ed10b 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ActivityOtherThemes.kt @@ -31,6 +31,11 @@ sealed class ActivityOtherThemes(@StyleRes val dark: Int, R.style.AppTheme_Black ) + object Launcher : ActivityOtherThemes( + R.style.AppTheme_Launcher, + R.style.AppTheme_Launcher + ) + object AttachmentsPreview : ActivityOtherThemes( R.style.AppTheme_AttachmentsPreview, R.style.AppTheme_AttachmentsPreview diff --git a/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml b/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml index 6dd94d0d75..fc57d53ab3 100644 --- a/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml +++ b/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml @@ -8,7 +8,9 @@ + android:layout_height="wrap_content" + android:paddingTop="32dp" + android:paddingBottom="32dp"> - - - - - - - - - + + + + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_or" /> - + \ No newline at end of file