From 63ef40f58b7717ea79ddd8260f22726bd5809594 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 17 Nov 2023 17:09:18 +0000 Subject: [PATCH 01/18] Fix issue with timeline message view reuse while rich text editor is enabled (#8688) --- changelog.d/8688.bugfix | 1 + .../detail/timeline/item/MessageTextItem.kt | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 changelog.d/8688.bugfix diff --git a/changelog.d/8688.bugfix b/changelog.d/8688.bugfix new file mode 100644 index 0000000000..0f746f1293 --- /dev/null +++ b/changelog.d/8688.bugfix @@ -0,0 +1 @@ +Fix issue with timeline message view reuse while rich text editor is enabled \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index a9cd25ae19..6ffaa6d896 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -85,8 +85,13 @@ abstract class MessageTextItem : AbsMessageItem() { } holder.previewUrlView.delegate = previewUrlCallback holder.previewUrlView.renderMessageLayout(attributes.informationData.messageLayout) - - val messageView: AppCompatTextView = if (useRichTextEditorStyle) holder.richMessageView else holder.plainMessageView + if (useRichTextEditorStyle) { + holder.plainMessageView?.isVisible = false + } else { + holder.richMessageView?.isVisible = false + } + val messageView: AppCompatTextView = if (useRichTextEditorStyle) holder.requireRichMessageView() else holder.requirePlainMessageView() + messageView.isVisible = true if (useBigFont) { messageView.textSize = 44F } else { @@ -133,11 +138,21 @@ abstract class MessageTextItem : AbsMessageItem() { val previewUrlView by bind(R.id.messageUrlPreview) private val richMessageStub by bind(R.id.richMessageTextViewStub) private val plainMessageStub by bind(R.id.plainMessageTextViewStub) - val richMessageView: AppCompatTextView by lazy { - richMessageStub.inflate().findViewById(R.id.messageTextView) + var richMessageView: AppCompatTextView? = null + private set + var plainMessageView: AppCompatTextView? = null + private set + + fun requireRichMessageView(): AppCompatTextView { + val view = richMessageView ?: richMessageStub.inflate().findViewById(R.id.messageTextView) + richMessageView = view + return view } - val plainMessageView: AppCompatTextView by lazy { - plainMessageStub.inflate().findViewById(R.id.messageTextView) + + fun requirePlainMessageView(): AppCompatTextView { + val view = plainMessageView ?: plainMessageStub.inflate().findViewById(R.id.messageTextView) + plainMessageView = view + return view } } From 84158ece370d543c32718f5a4a669b1323953b79 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Nov 2023 16:35:12 +0100 Subject: [PATCH 02/18] Ensure Background sync is not stopped when there is an active call. It was happening since the application is foregrounded when VectorCallActivity is displayed. --- changelog.d/4066.bugfix | 1 + .../java/im/vector/app/VectorApplication.kt | 24 +++++++++++++++---- .../features/call/webrtc/WebRtcCallManager.kt | 6 ++++- 3 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 changelog.d/4066.bugfix diff --git a/changelog.d/4066.bugfix b/changelog.d/4066.bugfix new file mode 100644 index 0000000000..7c97993511 --- /dev/null +++ b/changelog.d/4066.bugfix @@ -0,0 +1 @@ +Stop incoming call ringing if the call is cancelled or answered on another session. diff --git a/vector-app/src/main/java/im/vector/app/VectorApplication.kt b/vector-app/src/main/java/im/vector/app/VectorApplication.kt index 8d12292524..deb3d8d3aa 100644 --- a/vector-app/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector-app/src/main/java/im/vector/app/VectorApplication.kt @@ -108,6 +108,7 @@ class VectorApplication : @Inject lateinit var buildMeta: BuildMeta @Inject lateinit var leakDetector: LeakDetector @Inject lateinit var vectorLocale: VectorLocale + @Inject lateinit var webRtcCallManager: WebRtcCallManager // font thread handler private var fontThreadHandler: Handler? = null @@ -167,20 +168,33 @@ class VectorApplication : notificationUtils.createNotificationChannels() ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { + private var stopBackgroundSync = false + override fun onResume(owner: LifecycleOwner) { Timber.i("App entered foreground") fcmHelper.onEnterForeground(activeSessionHolder) - activeSessionHolder.getSafeActiveSessionAsync { - it?.syncService()?.stopAnyBackgroundSync() + if (webRtcCallManager.currentCall.get() == null) { + Timber.i("App entered foreground and no active call: stop any background sync") + activeSessionHolder.getSafeActiveSessionAsync { + it?.syncService()?.stopAnyBackgroundSync() + } + } else { + Timber.i("App entered foreground: there is an active call, set stopBackgroundSync to true") + stopBackgroundSync = true } -// activeSessionHolder.getSafeActiveSession()?.also { -// it.syncService().stopAnyBackgroundSync() -// } } override fun onPause(owner: LifecycleOwner) { Timber.i("App entered background") fcmHelper.onEnterBackground(activeSessionHolder) + + if (stopBackgroundSync) { + Timber.i("App entered background: stop any background sync") + activeSessionHolder.getSafeActiveSessionAsync { + it?.syncService()?.stopAnyBackgroundSync() + } + stopBackgroundSync = false + } } }) ProcessLifecycleOwner.get().lifecycle.addObserver(spaceStateHandler) diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 1eb4134a87..c432e7ebd4 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -139,6 +139,7 @@ class WebRtcCallManager @Inject constructor( private val rootEglBase by lazy { EglUtils.rootEglBase } private var isInBackground: Boolean = true + private var syncStartedWhenInBackground: Boolean = false override fun onResume(owner: LifecycleOwner) { isInBackground = false @@ -274,13 +275,15 @@ class WebRtcCallManager @Inject constructor( peerConnectionFactory = null audioManager.setMode(CallAudioManager.Mode.DEFAULT) // did we start background sync? so we should stop it - if (isInBackground) { + if (syncStartedWhenInBackground) { if (!unifiedPushHelper.isBackgroundSync()) { + Timber.tag(loggerTag.value).v("Sync started when in background, stop it") currentSession?.syncService()?.stopAnyBackgroundSync() } else { // for fdroid we should not stop, it should continue syncing // maybe we should restore default timeout/delay though? } + syncStartedWhenInBackground = false } } } @@ -383,6 +386,7 @@ class WebRtcCallManager @Inject constructor( if (isInBackground) { if (!unifiedPushHelper.isBackgroundSync()) { // only for push version as fdroid version is already doing it? + syncStartedWhenInBackground = true currentSession?.syncService()?.startAutomaticBackgroundSync(30, 0) } else { // Maybe increase sync freq? but how to set back to default values? From 8d85d047b7328f082da8d69fd09539ce7b893490 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 22 Nov 2023 09:42:39 +0100 Subject: [PATCH 03/18] Ensure Background sync is not stopped when there is an active call, even when the app goes to background. --- .../src/main/java/im/vector/app/VectorApplication.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/vector-app/src/main/java/im/vector/app/VectorApplication.kt b/vector-app/src/main/java/im/vector/app/VectorApplication.kt index deb3d8d3aa..7b41c12773 100644 --- a/vector-app/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector-app/src/main/java/im/vector/app/VectorApplication.kt @@ -189,11 +189,15 @@ class VectorApplication : fcmHelper.onEnterBackground(activeSessionHolder) if (stopBackgroundSync) { - Timber.i("App entered background: stop any background sync") - activeSessionHolder.getSafeActiveSessionAsync { - it?.syncService()?.stopAnyBackgroundSync() + if (webRtcCallManager.currentCall.get() == null) { + Timber.i("App entered background: stop any background sync") + activeSessionHolder.getSafeActiveSessionAsync { + it?.syncService()?.stopAnyBackgroundSync() + } + stopBackgroundSync = false + } else { + Timber.i("App entered background: there is an active call do not stop background sync") } - stopBackgroundSync = false } } }) From a6fafb07da037873f8f47d7e03b5dc7030191ef2 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Mon, 27 Nov 2023 16:30:44 +0000 Subject: [PATCH 04/18] Remove quote from message actions menu (#8689) --- .../room/detail/timeline/action/MessageActionsViewModel.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index d56bf18fa8..62aed5c3c6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -370,10 +370,6 @@ class MessageActionsViewModel @AssistedInject constructor( add(EventSharedAction.ViewReactions(informationData)) } - if (canQuote(timelineEvent, messageContent, actionPermissions)) { - add(EventSharedAction.Quote(eventId)) - } - if (timelineEvent.hasBeenEdited()) { add(EventSharedAction.ViewEditHistory(informationData)) } From 6ba03f82e40b2e47b44f97d1e8aae93d15655a74 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Nov 2023 17:12:03 +0100 Subject: [PATCH 05/18] Changelog for version 1.6.8 --- CHANGES.md | 18 ++++++++++++++++++ changelog.d/4066.bugfix | 1 - changelog.d/8178.bugfix | 1 - changelog.d/8658.misc | 1 - changelog.d/8662.misc | 1 - changelog.d/8671.misc | 1 - changelog.d/8679.misc | 1 - changelog.d/8683.bugfix | 1 - changelog.d/8688.bugfix | 1 - 9 files changed, 18 insertions(+), 8 deletions(-) delete mode 100644 changelog.d/4066.bugfix delete mode 100644 changelog.d/8178.bugfix delete mode 100644 changelog.d/8658.misc delete mode 100644 changelog.d/8662.misc delete mode 100644 changelog.d/8671.misc delete mode 100644 changelog.d/8679.misc delete mode 100644 changelog.d/8683.bugfix delete mode 100644 changelog.d/8688.bugfix diff --git a/CHANGES.md b/CHANGES.md index 6996863716..5f685efc75 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,21 @@ +Changes in Element v1.6.8 (2023-11-28) +====================================== + +Bugfixes ๐Ÿ› +---------- + - Stop incoming call ringing if the call is cancelled or answered on another session. ([#4066](https://github.com/vector-im/element-android/issues/4066)) + - Ensure the incoming call will not ring forever, in case the call is not ended by another way. ([#8178](https://github.com/vector-im/element-android/issues/8178)) + - Unified Push: Ignore the potential SSL error when the custom gateway is testing locally ([#8683](https://github.com/vector-im/element-android/issues/8683)) + - Fix issue with timeline message view reuse while rich text editor is enabled ([#8688](https://github.com/vector-im/element-android/issues/8688)) + +Other changes +------------- + - Remove unused WebRTC dependency ([#8658](https://github.com/vector-im/element-android/issues/8658)) + - Take into account boolean "io.element.disable_network_constraint" from the .well-known file. ([#8662](https://github.com/vector-im/element-android/issues/8662)) + - Update regex for email address to be aligned on RFC 5322 ([#8671](https://github.com/vector-im/element-android/issues/8671)) + - Bump crypto sdk bindings to v0.3.16 ([#8679](https://github.com/vector-im/element-android/issues/8679)) + + Changes in Element v1.6.6 (2023-10-05) ====================================== diff --git a/changelog.d/4066.bugfix b/changelog.d/4066.bugfix deleted file mode 100644 index 7c97993511..0000000000 --- a/changelog.d/4066.bugfix +++ /dev/null @@ -1 +0,0 @@ -Stop incoming call ringing if the call is cancelled or answered on another session. diff --git a/changelog.d/8178.bugfix b/changelog.d/8178.bugfix deleted file mode 100644 index e7f073f4fc..0000000000 --- a/changelog.d/8178.bugfix +++ /dev/null @@ -1 +0,0 @@ -Ensure the incoming call will not ring forever, in case the call is not ended by another way. diff --git a/changelog.d/8658.misc b/changelog.d/8658.misc deleted file mode 100644 index f4cf3d051d..0000000000 --- a/changelog.d/8658.misc +++ /dev/null @@ -1 +0,0 @@ -Remove unused WebRTC dependency diff --git a/changelog.d/8662.misc b/changelog.d/8662.misc deleted file mode 100644 index 4a18120301..0000000000 --- a/changelog.d/8662.misc +++ /dev/null @@ -1 +0,0 @@ -Take into account boolean "io.element.disable_network_constraint" from the .well-known file. diff --git a/changelog.d/8671.misc b/changelog.d/8671.misc deleted file mode 100644 index 345dc36fd6..0000000000 --- a/changelog.d/8671.misc +++ /dev/null @@ -1 +0,0 @@ -Update regex for email address to be aligned on RFC 5322 \ No newline at end of file diff --git a/changelog.d/8679.misc b/changelog.d/8679.misc deleted file mode 100644 index d913671219..0000000000 --- a/changelog.d/8679.misc +++ /dev/null @@ -1 +0,0 @@ -Bump crypto sdk bindings to v0.3.16 diff --git a/changelog.d/8683.bugfix b/changelog.d/8683.bugfix deleted file mode 100644 index 5ce4bf96b8..0000000000 --- a/changelog.d/8683.bugfix +++ /dev/null @@ -1 +0,0 @@ -Unified Push: Ignore the potential SSL error when the custom gateway is testing locally diff --git a/changelog.d/8688.bugfix b/changelog.d/8688.bugfix deleted file mode 100644 index 0f746f1293..0000000000 --- a/changelog.d/8688.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix issue with timeline message view reuse while rich text editor is enabled \ No newline at end of file From b27dc02cfdcb75b91dadc2ff9d331aca33492f75 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Nov 2023 17:13:37 +0100 Subject: [PATCH 06/18] Adding fastlane file for version 1.6.8 --- fastlane/metadata/android/en-US/changelogs/40106080.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/40106080.txt diff --git a/fastlane/metadata/android/en-US/changelogs/40106080.txt b/fastlane/metadata/android/en-US/changelogs/40106080.txt new file mode 100644 index 0000000000..68fa34470b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40106080.txt @@ -0,0 +1,2 @@ +Main changes in this version: Bugfixes. +Full changelog: https://github.com/vector-im/element-android/releases From f46a9d6cc8b5546f3bff2ddaca60ac46603cd789 Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 28 Nov 2023 17:17:44 +0100 Subject: [PATCH 07/18] Update release script now that there is only on crypto implementation. --- tools/release/releaseScript.sh | 52 +++++++++++++++++----------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index f198670eae..cf9671c1dc 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -160,11 +160,11 @@ adb -e uninstall im.vector.app.debug.test printf "\n================================================================================\n" printf "Running the integration test UiAllScreensSanityTest.allScreensTest()...\n" -./gradlew connectedGplayRustCryptoDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest +./gradlew connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest printf "\n================================================================================\n" printf "Building the app...\n" -./gradlew assembleGplayRustCryptoDebug +./gradlew assembleGplayDebug printf "\n================================================================================\n" printf "Uninstalling previous debug app if any...\n" @@ -172,7 +172,7 @@ adb -e uninstall im.vector.app.debug printf "\n================================================================================\n" printf "Installing the app...\n" -adb -e install ./vector-app/build/outputs/apk/gplayRustCrypto/debug/vector-gplay-rustCrypto-arm64-v8a-debug.apk +adb -e install ./vector-app/build/outputs/apk/gplay/debug/vector-gplay-arm64-v8a-debug.apk printf "\n================================================================================\n" printf "Running the app...\n" @@ -293,67 +293,67 @@ printf "Unzipping the artifact...\n" unzip ${targetPath}/vector-gplay-release-unsigned.zip -d ${targetPath} # Flatten folder hierarchy -mv ${targetPath}/gplayRustCrypto/release/* ${targetPath} +mv ${targetPath}/gplay/release/* ${targetPath} rm -rf ${targetPath}/gplay printf "\n================================================================================\n" printf "Signing the APKs...\n" -cp ${targetPath}/vector-gplay-rustCrypto-arm64-v8a-release-unsigned.apk \ - ${targetPath}/vector-gplay-rustCrypto-arm64-v8a-release-signed.apk +cp ${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk \ + ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk ./tools/release/sign_apk_unsafe.sh \ ${keyStorePath} \ - ${targetPath}/vector-gplay-rustCrypto-arm64-v8a-release-signed.apk \ + ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk \ ${keyStorePassword} \ ${keyPassword} -cp ${targetPath}/vector-gplay-rustCrypto-armeabi-v7a-release-unsigned.apk \ - ${targetPath}/vector-gplay-rustCrypto-armeabi-v7a-release-signed.apk +cp ${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk \ + ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk ./tools/release/sign_apk_unsafe.sh \ ${keyStorePath} \ - ${targetPath}/vector-gplay-rustCrypto-armeabi-v7a-release-signed.apk \ + ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk \ ${keyStorePassword} \ ${keyPassword} -cp ${targetPath}/vector-gplay-rustCrypto-x86-release-unsigned.apk \ - ${targetPath}/vector-gplay-rustCrypto-x86-release-signed.apk +cp ${targetPath}/vector-gplay-x86-release-unsigned.apk \ + ${targetPath}/vector-gplay-x86-release-signed.apk ./tools/release/sign_apk_unsafe.sh \ ${keyStorePath} \ - ${targetPath}/vector-gplay-rustCrypto-x86-release-signed.apk \ + ${targetPath}/vector-gplay-x86-release-signed.apk \ ${keyStorePassword} \ ${keyPassword} -cp ${targetPath}/vector-gplay-rustCrypto-x86_64-release-unsigned.apk \ - ${targetPath}/vector-gplay-rustCrypto-x86_64-release-signed.apk +cp ${targetPath}/vector-gplay-x86_64-release-unsigned.apk \ + ${targetPath}/vector-gplay-x86_64-release-signed.apk ./tools/release/sign_apk_unsafe.sh \ ${keyStorePath} \ - ${targetPath}/vector-gplay-rustCrypto-x86_64-release-signed.apk \ + ${targetPath}/vector-gplay-x86_64-release-signed.apk \ ${keyStorePassword} \ ${keyPassword} # Ref: https://docs.fastlane.tools/getting-started/android/beta-deployment/#uploading-your-app -# set SUPPLY_APK_PATHS="${targetPath}/vector-gplay-rustCrypto-arm64-v8a-release-unsigned.apk,${targetPath}/vector-gplay-rustCrypto-armeabi-v7a-release-unsigned.apk,${targetPath}/vector-gplay-rustCrypto-x86-release-unsigned.apk,${targetPath}/vector-gplay-rustCrypto-x86_64-release-unsigned.apk" +# set SUPPLY_APK_PATHS="${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk,${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk,${targetPath}/vector-gplay-x86-release-unsigned.apk,${targetPath}/vector-gplay-x86_64-release-unsigned.apk" # # ./fastlane beta printf "\n================================================================================\n" printf "Please check the information below:\n" -printf "File vector-gplay-rustCrypto-arm64-v8a-release-signed.apk:\n" -${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-rustCrypto-arm64-v8a-release-signed.apk | grep package -printf "File vector-gplay-rustCrypto-armeabi-v7a-release-signed.apk:\n" -${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-rustCrypto-armeabi-v7a-release-signed.apk | grep package -printf "File vector-gplay-rustCrypto-x86-release-signed.apk:\n" -${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-rustCrypto-x86-release-signed.apk | grep package -printf "File vector-gplay-rustCrypto-x86_64-release-signed.apk:\n" -${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-rustCrypto-x86_64-release-signed.apk | grep package +printf "File vector-gplay-arm64-v8a-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk | grep package +printf "File vector-gplay-armeabi-v7a-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk | grep package +printf "File vector-gplay-x86-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86-release-signed.apk | grep package +printf "File vector-gplay-x86_64-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86_64-release-signed.apk | grep package printf "\n" read -p "Does it look correct? Press enter when it's done." printf "\n================================================================================\n" read -p "Installing apk on a real device, press enter when a real device is connected. " -apkPath="${targetPath}/vector-gplay-rustCrypto-arm64-v8a-release-signed.apk" +apkPath="${targetPath}/vector-gplay-arm64-v8a-release-signed.apk" adb -d install ${apkPath} read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done." From bb9d1fc8d88f3b128bb8e01fc897b0a3f2d86d81 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Nov 2023 17:23:40 +0100 Subject: [PATCH 08/18] version++ --- matrix-sdk-android/build.gradle | 2 +- vector-app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index cab81b0283..84b05a527a 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.6.8\"" + buildConfigField "String", "SDK_VERSION", "\"1.6.10\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 7320465d21..7c51dcd34c 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 6 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 8 +ext.versionPatch = 10 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' From fb077d5065629bf40c57909ae296d57bbf3259ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Nov 2023 18:02:02 +0100 Subject: [PATCH 09/18] Implement Mobile Device Manager feature with 3 keys. --- .../fcm/VectorFirebaseMessagingService.kt | 12 +++- vector-app/src/main/AndroidManifest.xml | 4 ++ .../im/vector/app/core/di/SingletonModule.kt | 10 ++++ vector-app/src/main/res/values/strings.xml | 4 ++ .../main/res/xml/vector_app_restrictions.xml | 18 ++++++ .../app/core/platform/VectorBaseActivity.kt | 7 +++ .../vector/app/core/pushers/PushersManager.kt | 8 ++- .../app/core/pushers/UnifiedPushHelper.kt | 20 ++++++- .../homeserver/ServerUrlsRepository.kt | 3 +- .../app/features/mdm/DefaultMdmService.kt | 56 +++++++++++++++++++ .../im/vector/app/features/mdm/MdmService.kt | 39 +++++++++++++ .../onboarding/OnboardingViewModel.kt | 5 +- 12 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 vector-app/src/main/res/xml/vector_app_restrictions.xml create mode 100644 vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt create mode 100644 vector/src/main/java/im/vector/app/features/mdm/MdmService.kt diff --git a/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt b/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt index 6ab9b90a84..7a7f14d690 100644 --- a/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt +++ b/vector-app/src/gplay/java/im/vector/app/push/fcm/VectorFirebaseMessagingService.kt @@ -26,6 +26,8 @@ import im.vector.app.core.pushers.PushParser import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.pushers.VectorPushHandler +import im.vector.app.features.mdm.MdmData +import im.vector.app.features.mdm.MdmService import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -46,6 +48,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var pushParser: PushParser @Inject lateinit var vectorPushHandler: VectorPushHandler @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + @Inject lateinit var mdmService: MdmService private val scope = CoroutineScope(SupervisorJob()) @@ -53,6 +56,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { scope.cancel() super.onDestroy() } + override fun onNewToken(token: String) { Timber.tag(loggerTag.value).d("New Firebase token") fcmHelper.storeFcmToken(token) @@ -62,7 +66,13 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { unifiedPushHelper.isEmbeddedDistributor() ) { scope.launch { - pushersManager.enqueueRegisterPusher(token, getString(R.string.pusher_http_url)) + pushersManager.enqueueRegisterPusher( + pushKey = token, + gateway = mdmService.getData( + mdmData = MdmData.DefaultPushGatewayUrl, + defaultValue = getString(R.string.pusher_http_url), + ), + ) } } } diff --git a/vector-app/src/main/AndroidManifest.xml b/vector-app/src/main/AndroidManifest.xml index 68325ab512..3668296382 100644 --- a/vector-app/src/main/AndroidManifest.xml +++ b/vector-app/src/main/AndroidManifest.xml @@ -20,6 +20,10 @@ tools:ignore="UnusedAttribute" tools:replace="android:allowBackup"> + + ignored + + Default homeserver URL + Default Push gateway + Permalink base url diff --git a/vector-app/src/main/res/xml/vector_app_restrictions.xml b/vector-app/src/main/res/xml/vector_app_restrictions.xml new file mode 100644 index 0000000000..1932d19e5c --- /dev/null +++ b/vector-app/src/main/res/xml/vector_app_restrictions.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 1e29dfff5e..5bdd960e00 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -76,6 +76,7 @@ import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.consent.ConsentNotGivenHelper +import im.vector.app.features.mdm.MdmService import im.vector.app.features.navigation.Navigator import im.vector.app.features.pin.PinLocker import im.vector.app.features.pin.PinMode @@ -171,6 +172,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var errorFormatter: ErrorFormatter + @Inject lateinit var mdmService: MdmService // For debug only @Inject lateinit var debugReceiver: DebugReceiver @@ -412,6 +414,10 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver rageShake.start() } debugReceiver.register(this) + mdmService.registerListener(this) { + // Just log that a change occurred. + Timber.w("MDM data has been updated") + } } private val postResumeScheduledActions = mutableListOf<() -> Unit>() @@ -442,6 +448,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver rageShake.stop() debugReceiver.unregister(this) + mdmService.unregisterListener(this) } override fun onWindowFocusChanged(hasFocus: Boolean) { diff --git a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt index 402471ecef..1f2441622d 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/PushersManager.kt @@ -22,6 +22,8 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.AppNameProvider import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.features.mdm.MdmData +import im.vector.app.features.mdm.MdmService import org.matrix.android.sdk.api.session.pushers.HttpPusher import org.matrix.android.sdk.api.session.pushers.Pusher import java.util.UUID @@ -37,6 +39,7 @@ class PushersManager @Inject constructor( private val stringProvider: StringProvider, private val appNameProvider: AppNameProvider, private val getDeviceInfoUseCase: GetDeviceInfoUseCase, + private val mdmService: MdmService, ) { suspend fun testPush() { val currentSession = activeSessionHolder.getActiveSession() @@ -50,7 +53,10 @@ class PushersManager @Inject constructor( } suspend fun enqueueRegisterPusherWithFcmKey(pushKey: String): UUID { - return enqueueRegisterPusher(pushKey, stringProvider.getString(R.string.pusher_http_url)) + return enqueueRegisterPusher( + pushKey = pushKey, + gateway = mdmService.getData(MdmData.DefaultPushGatewayUrl, stringProvider.getString(R.string.pusher_http_url)) + ) } suspend fun enqueueRegisterPusher( diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 07052c7146..5b6d91e354 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -24,6 +24,8 @@ import com.squareup.moshi.JsonClass import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.getApplicationLabel +import im.vector.app.features.mdm.MdmData +import im.vector.app.features.mdm.MdmService import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.failure.Failure @@ -40,6 +42,7 @@ class UnifiedPushHelper @Inject constructor( private val stringProvider: StringProvider, private val matrix: Matrix, private val fcmHelper: FcmHelper, + private val mdmService: MdmService, ) { @MainThread @@ -99,7 +102,12 @@ class UnifiedPushHelper @Inject constructor( // register app_id type upfcm on sygnal // the pushkey if FCM key if (UnifiedPush.getDistributor(context) == context.packageName) { - unifiedPushStore.storePushGateway(stringProvider.getString(R.string.pusher_http_url)) + unifiedPushStore.storePushGateway( + gateway = mdmService.getData( + mdmData = MdmData.DefaultPushGatewayUrl, + defaultValue = stringProvider.getString(R.string.pusher_http_url), + ) + ) onDoneRunnable?.run() return } @@ -185,7 +193,13 @@ class UnifiedPushHelper @Inject constructor( } fun getPushGateway(): String? { - return if (isEmbeddedDistributor()) stringProvider.getString(R.string.pusher_http_url) - else unifiedPushStore.getPushGateway() + return if (isEmbeddedDistributor()) { + mdmService.getData( + mdmData = MdmData.DefaultPushGatewayUrl, + defaultValue = stringProvider.getString(R.string.pusher_http_url), + ) + } else { + unifiedPushStore.getPushGateway() + } } } diff --git a/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt b/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt index 636c557da9..5b1496028d 100644 --- a/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt +++ b/vector/src/main/java/im/vector/app/features/homeserver/ServerUrlsRepository.kt @@ -25,6 +25,7 @@ import javax.inject.Inject /** * Object to store and retrieve home and identity server urls. + * Note: this class is not used. */ class ServerUrlsRepository @Inject constructor( @DefaultPreferences @@ -89,5 +90,5 @@ class ServerUrlsRepository @Inject constructor( /** * Return default homeserver url from resources. */ - fun getDefaultHomeServerUrl() = stringProvider.getString(R.string.matrix_org_server_url) + private fun getDefaultHomeServerUrl() = stringProvider.getString(R.string.matrix_org_server_url) } diff --git a/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt b/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt new file mode 100644 index 0000000000..0523a1dd41 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.mdm + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.RestrictionsManager +import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext +import timber.log.Timber +import javax.inject.Inject + +class DefaultMdmService @Inject constructor( + @ApplicationContext applicationContext: Context +) : MdmService { + private val restrictionsManager = applicationContext.getSystemService() + private var onChangedListener: (() -> Unit)? = null + + private val restrictionsReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Timber.w("Restrictions changed") + onChangedListener?.invoke() + } + } + + override fun registerListener(context: Context, onChangedListener: () -> Unit) { + val restrictionsFilter = IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED) + this.onChangedListener = onChangedListener + context.registerReceiver(restrictionsReceiver, restrictionsFilter) + } + + override fun unregisterListener(context: Context) { + context.unregisterReceiver(restrictionsReceiver) + this.onChangedListener = null + } + + override fun getData(mdmData: MdmData): String? { + return restrictionsManager?.applicationRestrictions?.getString(mdmData.key) + } +} diff --git a/vector/src/main/java/im/vector/app/features/mdm/MdmService.kt b/vector/src/main/java/im/vector/app/features/mdm/MdmService.kt new file mode 100644 index 0000000000..d601c1658a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/mdm/MdmService.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.mdm + +import android.content.Context +import timber.log.Timber + +enum class MdmData(val key: String) { + DefaultHomeserverUrl(key = "im.vector.app.serverConfigDefaultHomeserverUrlString"), + DefaultPushGatewayUrl(key = "im.vector.app.serverConfigSygnalAPIUrlString"), + PermalinkBaseUrl(key = "im.vector.app.clientPermalinkBaseUrl"), +} + +interface MdmService { + fun registerListener(context: Context, onChangedListener: () -> Unit) + fun unregisterListener(context: Context) + fun getData(mdmData: MdmData): String? + fun getData(mdmData: MdmData, defaultValue: String): String { + return getData(mdmData) + ?.also { + Timber.w("Using MDM data for ${mdmData.name}: $it") + } + ?: defaultValue + } +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index fc6878ffd2..74427736d7 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -46,6 +46,8 @@ import im.vector.app.features.login.LoginMode import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode +import im.vector.app.features.mdm.MdmData +import im.vector.app.features.mdm.MdmService import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult import kotlinx.coroutines.Job @@ -93,6 +95,7 @@ class OnboardingViewModel @AssistedInject constructor( private val registrationActionHandler: RegistrationActionHandler, private val sdkIntProvider: BuildVersionSdkIntProvider, private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase, + mdmService: MdmService, ) : VectorViewModel(initialState) { @AssistedFactory @@ -143,7 +146,7 @@ class OnboardingViewModel @AssistedInject constructor( } private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() - private val defaultHomeserverUrl = matrixOrgUrl + private val defaultHomeserverUrl = mdmService.getData(MdmData.DefaultHomeserverUrl, matrixOrgUrl) private val registrationWizard: RegistrationWizard get() = authenticationService.getRegistrationWizard() From 5987ca9c48ffdd6dc7c4747d411c2b59038196a9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 30 Nov 2023 18:24:58 +0100 Subject: [PATCH 10/18] changelog --- changelog.d/8698.feature | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog.d/8698.feature diff --git a/changelog.d/8698.feature b/changelog.d/8698.feature new file mode 100644 index 0000000000..efe11f03cb --- /dev/null +++ b/changelog.d/8698.feature @@ -0,0 +1,5 @@ +Add support for Mobile Device Management. +The keys are: +- default homeserver URL `im.vector.app.serverConfigDefaultHomeserverUrlString` +- push gateway URL `im.vector.app.serverConfigSygnalAPIUrlString` +- permalink base URL `im.vector.app.clientPermalinkBaseUrl` From 8e0c503b456498227869d33960ac94bcc563d397 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 28 Nov 2023 17:23:40 +0100 Subject: [PATCH 11/18] Support Functional members #3736 --- changelog.d/3736.feature | 1 + .../TestRoomDisplayNameFallbackProvider.kt | 2 ++ .../RoomDisplayNameFallbackProvider.kt | 4 +++ .../session/room/RoomAvatarResolver.kt | 23 ++++++++---- .../membership/RoomDisplayNameResolver.kt | 5 ++- .../main/java/im/vector/app/config/Config.kt | 4 +++ .../features/room/FunctionalMembersState.kt | 36 +++++++++++++++++++ .../VectorRoomDisplayNameFallbackProvider.kt | 15 +++++++- 8 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 changelog.d/3736.feature create mode 100644 vector/src/main/java/im/vector/app/features/room/FunctionalMembersState.kt diff --git a/changelog.d/3736.feature b/changelog.d/3736.feature new file mode 100644 index 0000000000..1c89c6d846 --- /dev/null +++ b/changelog.d/3736.feature @@ -0,0 +1 @@ +Support Functional members diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt index a74f5010c2..773f480b5d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestRoomDisplayNameFallbackProvider.kt @@ -20,6 +20,8 @@ import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider { + override fun excludedUserIds(roomId: String) = emptyList() + override fun getNameForRoomInvite() = "Room invite" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt index 37d9b46b0b..6dc9f315a4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/provider/RoomDisplayNameFallbackProvider.kt @@ -25,6 +25,10 @@ package org.matrix.android.sdk.api.provider * *Limitation*: if the locale of the device changes, the methods will not be called again. */ interface RoomDisplayNameFallbackProvider { + /** + * Return the list of user ids to ignore when computing the room display name. + */ + fun excludedUserIds(roomId: String): List fun getNameForRoomInvite(): String fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List): String fun getNameFor1member(name: String): String diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt index c3d55b267a..742e4b8ec7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAvatarResolver.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room import io.realm.Realm +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -31,7 +32,12 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import javax.inject.Inject -internal class RoomAvatarResolver @Inject constructor(@UserId private val userId: String) { +internal class RoomAvatarResolver @Inject constructor( + matrixConfiguration: MatrixConfiguration, + @UserId private val userId: String +) { + + private val roomDisplayNameFallbackProvider = matrixConfiguration.roomDisplayNameFallbackProvider /** * Compute the room avatar url. @@ -40,21 +46,26 @@ internal class RoomAvatarResolver @Inject constructor(@UserId private val userId * @return the room avatar url, can be a fallback to a room member avatar or null */ fun resolve(realm: Realm, roomId: String): String? { - val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "") + val roomAvatarUrl = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "") ?.root ?.asDomain() ?.content ?.toModel() ?.avatarUrl - if (!roomName.isNullOrEmpty()) { - return roomName + if (!roomAvatarUrl.isNullOrEmpty()) { + return roomAvatarUrl } - val roomMembers = RoomMemberHelper(realm, roomId) - val members = roomMembers.queryActiveRoomMembersEvent().findAll() // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) val isDirectRoom = RoomSummaryEntity.where(realm, roomId).findFirst()?.isDirect.orFalse() if (isDirectRoom) { + val excludedUserIds = roomDisplayNameFallbackProvider.excludedUserIds(roomId) + val roomMembers = RoomMemberHelper(realm, roomId) + val members = roomMembers + .queryActiveRoomMembersEvent() + .not().`in`(RoomMemberSummaryEntityFields.USER_ID, excludedUserIds.toTypedArray()) + .findAll() + if (members.size == 1) { // Use avatar of a left user val firstLeftAvatarUrl = roomMembers.queryLeftRoomMembersEvent() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index 7497ecf21b..7a5b91a0ce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -92,18 +92,20 @@ internal class RoomDisplayNameResolver @Inject constructor( } ?: roomDisplayNameFallbackProvider.getNameForRoomInvite() } else if (roomEntity?.membership == Membership.JOIN) { + val excludedUserIds = roomDisplayNameFallbackProvider.excludedUserIds(roomId) val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val invitedCount = roomSummary?.invitedMembersCount ?: 0 val joinedCount = roomSummary?.joinedMembersCount ?: 0 val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { roomSummary.heroes.mapNotNull { userId -> roomMembers.getLastRoomMember(userId)?.takeIf { - it.membership == Membership.INVITE || it.membership == Membership.JOIN + (it.membership == Membership.INVITE || it.membership == Membership.JOIN) && !excludedUserIds.contains(it.userId) } } } else { activeMembers.where() .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) + .not().`in`(RoomMemberSummaryEntityFields.USER_ID, excludedUserIds.toTypedArray()) .limit(5) .findAll() .createSnapshot() @@ -113,6 +115,7 @@ internal class RoomDisplayNameResolver @Inject constructor( 0 -> { // Get left members if any val leftMembersNames = roomMembers.queryLeftRoomMembersEvent() + .not().`in`(RoomMemberSummaryEntityFields.USER_ID, excludedUserIds.toTypedArray()) .findAll() .map { displayNameResolver.getBestName(it.toMatrixItem()) } val directUserId = roomSummary?.directUserId diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt index e51d09e13b..ae7af83053 100644 --- a/vector-config/src/main/java/im/vector/app/config/Config.kt +++ b/vector-config/src/main/java/im/vector/app/config/Config.kt @@ -42,6 +42,10 @@ object Config { const val ENABLE_LOCATION_SHARING = true const val LOCATION_MAP_TILER_KEY = "fU3vlMsMn4Jb6dnEIFsx" + /// Whether to read the `io.element.functional_members` state event + // and exclude any service members when computing a room's name and avatar. + const val SUPPORT_FUNCTIONAL_MEMBERS = true + /** * The maximum length of voice messages in milliseconds. */ diff --git a/vector/src/main/java/im/vector/app/features/room/FunctionalMembersState.kt b/vector/src/main/java/im/vector/app/features/room/FunctionalMembersState.kt new file mode 100644 index 0000000000..56c2ac26c5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/room/FunctionalMembersState.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.room + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.state.StateService + +private const val FUNCTIONAL_MEMBERS_STATE_EVENT_TYPE = "io.element.functional_members" + +@JsonClass(generateAdapter = true) +data class FunctionalMembersContent( + @Json(name = "service_members") val userIds: List +) + +fun StateService.getFunctionalMembers(): List { + return getStateEvent(FUNCTIONAL_MEMBERS_STATE_EVENT_TYPE, QueryStringValue.IsEmpty)?.let { + it.content.toModel()?.userIds + }.orEmpty() +} diff --git a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt index cfbc2748ad..9606d1492d 100644 --- a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt +++ b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt @@ -18,13 +18,26 @@ package im.vector.app.features.room import android.content.Context import im.vector.app.R +import im.vector.app.config.Config +import im.vector.app.core.di.ActiveSessionHolder import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider +import org.matrix.android.sdk.api.session.getRoom import javax.inject.Inject +import javax.inject.Provider class VectorRoomDisplayNameFallbackProvider @Inject constructor( - private val context: Context + private val context: Context, + private val activeSessionHolder: Provider, ) : RoomDisplayNameFallbackProvider { + override fun excludedUserIds(roomId: String): List { + if (!Config.SUPPORT_FUNCTIONAL_MEMBERS) return emptyList() + return activeSessionHolder.get().getSafeActiveSession() + ?.getRoom(roomId)?.let { room -> + room.stateService().getFunctionalMembers() + }.orEmpty() + } + override fun getNameForRoomInvite(): String { return context.getString(R.string.room_displayname_room_invite) } From 778dab7bb7b3a7fadd24effd34d497e58e5b2e60 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 Dec 2023 09:20:25 +0100 Subject: [PATCH 12/18] Cleanup and safer Json model. --- .../src/main/java/im/vector/app/config/Config.kt | 6 ++++-- .../vector/app/features/room/FunctionalMembersState.kt | 10 ++++++---- .../room/VectorRoomDisplayNameFallbackProvider.kt | 10 ++++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt index ae7af83053..758d4e2a43 100644 --- a/vector-config/src/main/java/im/vector/app/config/Config.kt +++ b/vector-config/src/main/java/im/vector/app/config/Config.kt @@ -42,8 +42,10 @@ object Config { const val ENABLE_LOCATION_SHARING = true const val LOCATION_MAP_TILER_KEY = "fU3vlMsMn4Jb6dnEIFsx" - /// Whether to read the `io.element.functional_members` state event - // and exclude any service members when computing a room's name and avatar. + /** + * Whether to read the `io.element.functional_members` state event + * and exclude any service members when computing a room's name and avatar. + */ const val SUPPORT_FUNCTIONAL_MEMBERS = true /** diff --git a/vector/src/main/java/im/vector/app/features/room/FunctionalMembersState.kt b/vector/src/main/java/im/vector/app/features/room/FunctionalMembersState.kt index 56c2ac26c5..821acf948c 100644 --- a/vector/src/main/java/im/vector/app/features/room/FunctionalMembersState.kt +++ b/vector/src/main/java/im/vector/app/features/room/FunctionalMembersState.kt @@ -26,11 +26,13 @@ private const val FUNCTIONAL_MEMBERS_STATE_EVENT_TYPE = "io.element.functional_m @JsonClass(generateAdapter = true) data class FunctionalMembersContent( - @Json(name = "service_members") val userIds: List + @Json(name = "service_members") val userIds: List? = null ) fun StateService.getFunctionalMembers(): List { - return getStateEvent(FUNCTIONAL_MEMBERS_STATE_EVENT_TYPE, QueryStringValue.IsEmpty)?.let { - it.content.toModel()?.userIds - }.orEmpty() + return getStateEvent(FUNCTIONAL_MEMBERS_STATE_EVENT_TYPE, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + ?.userIds + .orEmpty() } diff --git a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt index 9606d1492d..57967d7a05 100644 --- a/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt +++ b/vector/src/main/java/im/vector/app/features/room/VectorRoomDisplayNameFallbackProvider.kt @@ -32,10 +32,12 @@ class VectorRoomDisplayNameFallbackProvider @Inject constructor( override fun excludedUserIds(roomId: String): List { if (!Config.SUPPORT_FUNCTIONAL_MEMBERS) return emptyList() - return activeSessionHolder.get().getSafeActiveSession() - ?.getRoom(roomId)?.let { room -> - room.stateService().getFunctionalMembers() - }.orEmpty() + return activeSessionHolder.get() + .getSafeActiveSession() + ?.getRoom(roomId) + ?.stateService() + ?.getFunctionalMembers() + .orEmpty() } override fun getNameForRoomInvite(): String { From 7e2b01b3fd1c2745f3a91d41801b1fa131ac056e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 Dec 2023 09:21:32 +0100 Subject: [PATCH 13/18] Add link to spec in changelog. --- changelog.d/3736.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/3736.feature b/changelog.d/3736.feature index 1c89c6d846..c7c7d151f0 100644 --- a/changelog.d/3736.feature +++ b/changelog.d/3736.feature @@ -1 +1 @@ -Support Functional members +Support Functional members (https://github.com/vector-im/element-meta/blob/develop/spec/functional_members.md) From 882020fdba8c7e10ef24a073f663e0a4783ecfbb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 Dec 2023 15:54:17 +0100 Subject: [PATCH 14/18] Fix test compilation issue. --- .../app/core/pushers/PushersManagerTest.kt | 2 ++ .../vector/app/features/mdm/NoOpMdmService.kt | 25 +++++++++++++++++++ .../onboarding/OnboardingViewModelTest.kt | 2 ++ 3 files changed, 29 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/mdm/NoOpMdmService.kt diff --git a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt index 4b6063fb93..48850c13c5 100644 --- a/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt +++ b/vector/src/test/java/im/vector/app/core/pushers/PushersManagerTest.kt @@ -17,6 +17,7 @@ package im.vector.app.core.pushers import im.vector.app.R +import im.vector.app.features.mdm.NoOpMdmService import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeAppNameProvider import im.vector.app.test.fakes.FakeGetDeviceInfoUseCase @@ -54,6 +55,7 @@ class PushersManagerTest { stringProvider.instance, appNameProvider, getDeviceInfoUseCase, + NoOpMdmService(), ) @Test diff --git a/vector/src/test/java/im/vector/app/features/mdm/NoOpMdmService.kt b/vector/src/test/java/im/vector/app/features/mdm/NoOpMdmService.kt new file mode 100644 index 0000000000..a8531d8c7f --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/mdm/NoOpMdmService.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.mdm + +import android.content.Context + +class NoOpMdmService : MdmService { + override fun registerListener(context: Context, onChangedListener: () -> Unit) = Unit + override fun unregisterListener(context: Context) = Unit + override fun getData(mdmData: MdmData): String? = null +} diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index a4afab8488..918452b6af 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -26,6 +26,7 @@ import im.vector.app.features.login.LoginMode import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode +import im.vector.app.features.mdm.NoOpMdmService import im.vector.app.features.onboarding.RegistrationStateFixture.aRegistrationState import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult import im.vector.app.test.TestBuildVersionSdkIntProvider @@ -1121,6 +1122,7 @@ class OnboardingViewModelTest { fakeRegistrationActionHandler.instance, TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.O }, fakeConfigureAndStartSessionUseCase, + NoOpMdmService() ).also { viewModel = it initialState = state From 6452b5c2b450da8de1f6ee4717b9940b49616b1e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 Dec 2023 17:54:48 +0100 Subject: [PATCH 15/18] Make DefaultMdmService a singleton and use Binds instead of Provides. --- .../main/java/im/vector/app/core/di/SingletonModule.kt | 8 +++----- .../java/im/vector/app/features/mdm/DefaultMdmService.kt | 2 ++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt index 0c250eedb8..9c173268cc 100644 --- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -111,6 +111,9 @@ import javax.inject.Singleton @Binds abstract fun bindEmojiSpanify(emojiCompatWrapper: EmojiCompatWrapper): EmojiSpanify + @Binds + abstract fun bindMdmService(service: DefaultMdmService): MdmService + @Binds abstract fun bindFontScale(fontScale: FontScalePreferencesImpl): FontScalePreferences @@ -173,11 +176,6 @@ import javax.inject.Singleton return Matrix(context, configuration) } - @Provides - fun providesMdmService(context: Context): MdmService { - return DefaultMdmService(context) - } - @Provides fun providesCurrentSession(activeSessionHolder: ActiveSessionHolder): Session { // TODO handle session injection better diff --git a/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt b/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt index 0523a1dd41..6633f3ce06 100644 --- a/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt +++ b/vector/src/main/java/im/vector/app/features/mdm/DefaultMdmService.kt @@ -25,7 +25,9 @@ import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import timber.log.Timber import javax.inject.Inject +import javax.inject.Singleton +@Singleton class DefaultMdmService @Inject constructor( @ApplicationContext applicationContext: Context ) : MdmService { From bb866601ef6244e98daa42bb9db0343d1dc736f2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 5 Dec 2023 14:14:05 +0100 Subject: [PATCH 16/18] Fix test compilation issue. --- .../vector/app/core/utils/TestMatrixHelper.kt | 3 +- .../TestRoomDisplayNameFallbackProvider.kt | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 vector-app/src/androidTest/java/im/vector/app/core/utils/TestRoomDisplayNameFallbackProvider.kt diff --git a/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt b/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt index d8873a71a4..44ea65244b 100644 --- a/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt +++ b/vector-app/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt @@ -17,7 +17,6 @@ package im.vector.app.core.utils import androidx.test.platform.app.InstrumentationRegistry -import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.SyncConfig @@ -25,7 +24,7 @@ import org.matrix.android.sdk.api.SyncConfig fun getMatrixInstance(): Matrix { val context = InstrumentationRegistry.getInstrumentation().targetContext val configuration = MatrixConfiguration( - roomDisplayNameFallbackProvider = VectorRoomDisplayNameFallbackProvider(context), + roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(), syncConfig = SyncConfig(longPollTimeout = 5_000L), ) return Matrix(context, configuration) diff --git a/vector-app/src/androidTest/java/im/vector/app/core/utils/TestRoomDisplayNameFallbackProvider.kt b/vector-app/src/androidTest/java/im/vector/app/core/utils/TestRoomDisplayNameFallbackProvider.kt new file mode 100644 index 0000000000..3d473d15d9 --- /dev/null +++ b/vector-app/src/androidTest/java/im/vector/app/core/utils/TestRoomDisplayNameFallbackProvider.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.utils + +import org.matrix.android.sdk.api.provider.RoomDisplayNameFallbackProvider + +class TestRoomDisplayNameFallbackProvider : RoomDisplayNameFallbackProvider { + + override fun excludedUserIds(roomId: String) = emptyList() + + override fun getNameForRoomInvite() = + "Room invite" + + override fun getNameForEmptyRoom(isDirect: Boolean, leftMemberNames: List) = + "Empty room" + + override fun getNameFor1member(name: String) = + name + + override fun getNameFor2members(name1: String, name2: String) = + "$name1 and $name2" + + override fun getNameFor3members(name1: String, name2: String, name3: String) = + "$name1, $name2 and $name3" + + override fun getNameFor4members(name1: String, name2: String, name3: String, name4: String) = + "$name1, $name2, $name3 and $name4" + + override fun getNameFor4membersAndMore(name1: String, name2: String, name3: String, remainingCount: Int) = + "$name1, $name2, $name3 and $remainingCount others" +} From 4d5fefa105fd5756b2ab02d441cfe8210a3c5eaa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 6 Dec 2023 14:57:50 +0100 Subject: [PATCH 17/18] Fix issue on chinese language resource. --- tools/import_sas_strings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/import_sas_strings.py b/tools/import_sas_strings.py index cd642fd0d1..6f63df2c84 100755 --- a/tools/import_sas_strings.py +++ b/tools/import_sas_strings.py @@ -96,7 +96,8 @@ write_file(os.path.join(data_defs_dir, "values/strings_sas.xml"), default) for lang in cumul: androidLang = lang\ .replace("_", "-r")\ - .replace("zh-rHans", "zh-rCN") + .replace("zh-rHans", "zh-rCN") \ + .replace("zh-rHant", "zh-rTW") write_file(os.path.join(data_defs_dir, "values-" + androidLang + "/strings_sas.xml"), cumul[lang]) print() From 9c6fbcd14189c16bec4120f90387aa4b939e5882 Mon Sep 17 00:00:00 2001 From: bmarty Date: Mon, 11 Dec 2023 00:03:20 +0000 Subject: [PATCH 18/18] Sync SAS Strings --- .../main/res/values-zh-rTW/strings_sas.xml | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 matrix-sdk-android/src/main/res/values-zh-rTW/strings_sas.xml diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings_sas.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings_sas.xml new file mode 100644 index 0000000000..fa4e49776b --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings_sas.xml @@ -0,0 +1,68 @@ + + + + ็‹— + ่ฒ“ + ็…ๅญ + ้ฆฌ + ็จ่ง’็ธ + ่ฑฌ + ๅคง่ฑก + ๅ…”ๅญ + ็†Š่ฒ“ + ๅ…ฌ้›ž + ไผ้ต + ็ƒ้พœ + ้ญš + ็ซ ้ญš + ่ด่ถ + ่Šฑ + ๆจน + ไป™ไบบๆŽŒ + ่˜‘่‡ + ๅœฐ็ƒ + ๆœˆไบฎ + ้›ฒๆœต + ็ซ + ้ฆ™่•‰ + ่˜‹ๆžœ + ่‰่Ž“ + ็Ž‰็ฑณ + ๆŠซ่–ฉ + ่›‹็ณ• + ๆ„›ๅฟƒ + ็ฌ‘่‡‰ + ๆฉŸๅ™จไบบ + ๅธฝๅญ + ็œผ้ก + ๆ‰ณๆ‰‹ + ่–่ช•่€ไบบ + ่ฎš + ้›จๅ‚˜ + ๆฒ™ๆผ + ๆ™‚้˜ + ็ฆฎ็‰ฉ + ็‡ˆๆณก + ๆ›ธ + ้‰›็ญ† + ่ฟด็ด‹้‡ + ๅ‰ชๅˆ€ + ้Ž–้ ญ + ้‘ฐๅŒ™ + ้Žšๅญ + ้›ป่ฉฑ + ๆ——ๅนŸ + ็ซ่ปŠ + ่…ณ่ธ่ปŠ + ้ฃ›ๆฉŸ + ็ซ็ฎญ + ็Ž็›ƒ + ่ถณ็ƒ + ๅ‰ไป– + ๅ–‡ๅญ + ้ˆด้บ + ่ˆน้Œจ + ่€ณๆฉŸ + ่ณ‡ๆ–™ๅคพ + ๅœ–้‡˜ +