diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml index c0e7caba76..ab0d0da9ba 100644 --- a/.github/workflows/post-pr.yml +++ b/.github/workflows/post-pr.yml @@ -31,7 +31,7 @@ jobs: ui-tests: name: UI Tests (Synapse) needs: should-i-run - runs-on: buildjet-4vcpu-ubuntu-2204 + runs-on: ubuntu-22.04 timeout-minutes: 90 # We might need to increase it if the time for tests grows strategy: fail-fast: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 678ceaf11d..d090d80513 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,11 +1,7 @@ name: Test on: - pull_request: { } - push: - branches: [ main, develop ] - paths-ignore: - - '.github/**' + workflow_dispatch: # Enrich gradle.properties for CI/CD env: @@ -15,7 +11,7 @@ env: jobs: tests: name: Runs all tests - runs-on: buildjet-4vcpu-ubuntu-2204 + runs-on: ubuntu-22.04 timeout-minutes: 90 # We might need to increase it if the time for tests grows strategy: matrix: diff --git a/CHANGES.md b/CHANGES.md index 9547bc7652..d3cf064732 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,14 @@ +Changes in Element v1.6.44 (2025-08-06) +======================================= + +Other changes +------------- + - Hide the "Manually Verify by Text" option behind devtool flag. ([#9058](https://github.com/element-hq/element-android/issues/9058)) + - Change targetSdk to 35. ([#9051](https://github.com/element-hq/element-android/issues/9051)) + - Support room v12. ([#9065](https://github.com/element-hq/element-android/issues/9065)) + - Fix window insets. ([#9067](https://github.com/element-hq/element-android/issues/9067)) + + Changes in Element v1.6.42 (2025-06-10) ======================================= diff --git a/README.md b/README.md index 9be359428e..77342f493f 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,7 @@ # Element Android -Element Android is an Android Matrix Client provided by [Element](https://element.io/). The app can be run on every Android devices with Android OS Lollipop and more (API 21). - -It is a total rewrite of [Riot-Android](https://github.com/element-hq/riot-android) with a new user experience. +Element Classic Android is a previous-generation [Matrix](https://matrix.org/) client provided by [Element](https://element.io/). The app can be run on every Android devices with Android OS Lollipop and more (API 21). This client is still supported and receives security updates but no new features or usability enhancements are made. It is recommended to use [Element X](https://github.com/element-hq/element-x-android) that is the next-generation mobile app. [Get it on Google Play](https://play.google.com/store/apps/details?id=im.vector.app) [Get it on F-Droid](https://f-droid.org/app/im.vector.app) diff --git a/dependencies.gradle b/dependencies.gradle index a0ac311963..d3e839a118 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,13 +1,13 @@ ext.versions = [ 'minSdk' : 21, - 'compileSdk' : 34, - 'targetSdk' : 34, + 'compileSdk' : 35, + 'targetSdk' : 35, 'sourceCompat' : JavaVersion.VERSION_21, 'targetCompat' : JavaVersion.VERSION_21, 'jvmTarget' : "21", ] -def gradle = "8.4.2" +def gradle = "8.11.0" // Ref: https://kotlinlang.org/releases.html def kotlin = "1.9.24" def kotlinCoroutines = "1.8.1" @@ -27,7 +27,7 @@ def bigImageViewer = "1.8.1" def jjwt = "0.11.5" def vanniktechEmoji = "0.16.0" def sentry = "6.18.1" -def fragment = "1.8.1" +def fragment = "1.8.6" // Testing def mockk = "1.13.11" def espresso = "3.6.1" @@ -50,7 +50,7 @@ ext.libs = [ 'activity' : "androidx.activity:activity-ktx:1.9.0", 'appCompat' : "androidx.appcompat:appcompat:1.7.0", 'biometric' : "androidx.biometric:biometric:1.1.0", - 'core' : "androidx.core:core-ktx:1.10.1", + 'core' : "androidx.core:core-ktx:1.16.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.3.0", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.6", 'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment", diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 3024963bf8..282fa353e4 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -213,6 +213,7 @@ ext.groups = [ 'org.jitsi', 'org.json', 'org.jsoup', + 'org.jspecify', 'org.junit', 'org.junit.jupiter', 'org.junit.platform', diff --git a/fastlane/metadata/android/en-US/changelogs/40106440.txt b/fastlane/metadata/android/en-US/changelogs/40106440.txt new file mode 100644 index 0000000000..b09bc07aa2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40106440.txt @@ -0,0 +1,2 @@ +Main changes in this version: support room v12. +Full changelog: https://github.com/element-hq/element-android/releases \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 8cde2fc687..2433ae78cb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -42,4 +42,4 @@ signing.element.nightly.keyPassword=Secret # Customise the Lint version to use a more recent version than the one bundled with AGP # https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html -android.experimental.lint.version=8.6.0-alpha08 +android.experimental.lint.version=8.12.0-alpha08 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3..1b33c55baa 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 515ab9d5f1..78cb6e16a4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=f8b4f4772d302c8ff580bc40d0f56e715de69b163546944f787c87abf209c961 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip +distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 65dcd68d65..23d15a9367 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +85,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -133,10 +133,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,16 +200,20 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 6689b85bee..5eed7ee845 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt index 09785e50e7..55a3b5d511 100644 --- a/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt +++ b/library/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt @@ -131,12 +131,15 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi // the touch coordinates if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + @Suppress("DEPRECATION") window.setDecorFitsSystemWindows(false) // New API instead of SYSTEM_UI_FLAG_IMMERSIVE window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // New API instead of FLAG_TRANSLUCENT_STATUS + @Suppress("DEPRECATION") window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // new API instead of FLAG_TRANSLUCENT_NAVIGATION + @Suppress("DEPRECATION") window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) } else { @Suppress("DEPRECATION") @@ -318,6 +321,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi protected open fun shouldAnimateDismiss(): Boolean = true protected open fun animateClose() { + @Suppress("DEPRECATION") window.statusBarColor = Color.TRANSPARENT finish() } @@ -334,14 +338,17 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi // Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + @Suppress("DEPRECATION") window.setDecorFitsSystemWindows(false) // new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars()) // New API instead of SYSTEM_UI_FLAG_IMMERSIVE window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // New API instead of FLAG_TRANSLUCENT_STATUS + @Suppress("DEPRECATION") window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) // New API instead of FLAG_TRANSLUCENT_NAVIGATION + @Suppress("DEPRECATION") window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) } else { @Suppress("DEPRECATION") @@ -363,6 +370,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi systemUiVisibility = true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + @Suppress("DEPRECATION") window.setDecorFitsSystemWindows(false) } else { @Suppress("DEPRECATION") diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 803510ed76..f1922645ac 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -120,6 +120,7 @@ %1$s modified %2$s widget You modified %1$s widget + Owner Admin Moderator Default @@ -685,6 +686,7 @@ Leave room Are you sure you want to leave the room? This room is not public. You will not be able to rejoin without an invite. + You\'re the only admin of this room. Leaving it will mean no one has control over it. Direct Messages @@ -2383,6 +2385,7 @@ Invites Users + Owner in %1$s Admin in %1$s Moderator in %1$s Default in %1$s diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index bd397d1bc6..68a0e8a5fe 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -64,6 +64,9 @@ @color/element_accent_light + + + ?vctr_toolbar_background @color/element_accent_light @android:color/white @color/element_accent_light diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 94f09e0bf5..88dd677cb8 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Optional @@ -95,6 +96,10 @@ class FlowRoom(private val room: Room) { } } + fun liveRoomPowerLevels(): Flow { + return room.stateService().getRoomPowerLevelsLive().asFlow() + } + fun liveReadMarker(): Flow> { return room.readService().getReadMarkerLive().asFlow() } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index ce4f195637..e2014e3bfd 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.42\"" + buildConfigField "String", "SDK_VERSION", "\"1.6.44\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt index de661275a7..df17fbf4a8 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -40,8 +40,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams @@ -500,12 +500,12 @@ class SpaceHierarchyTest : InstrumentedTest { room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!) commonTestHelper.retryPeriodically { - val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!! + val roomPowerLevels = aliceSession.getRoom(bobRoomId)!! .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content ?.toModel() - ?.let { PowerLevelsHelper(it) } - powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT) + ?.let { RoomPowerLevels(it) } + roomPowerLevels!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT) } aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt index 0f7e9ca6a8..4ec809a6d0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt @@ -30,15 +30,21 @@ object MatrixPatterns { // Note: TLD is not mandatory (localhost, IP address...) private const val DOMAIN_REGEX = ":[A-Z0-9.-]+(:[0-9]{2,5})?" + private const val BASE_64_ALPHABET = "[0-9A-Za-z/\\+=]+" + private const val BASE_64_URL_SAFE_ALPHABET = "[0-9A-Za-z/\\-_]+" + // regex pattern to find matrix user ids in a string. // See https://matrix.org/docs/spec/appendices#historical-user-ids private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX" val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) // regex pattern to find room ids in a string. - private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX" + private const val MATRIX_ROOM_IDENTIFIER_REGEX = "^!.+$DOMAIN_REGEX$" private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = MATRIX_ROOM_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + private const val MATRIX_ROOM_IDENTIFIER_DOMAINLESS_REGEX = "!$BASE_64_URL_SAFE_ALPHABET" + private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER_DOMAINLESS = MATRIX_ROOM_IDENTIFIER_DOMAINLESS_REGEX.toRegex() + // regex pattern to find room aliases in a string. private const val MATRIX_ROOM_ALIAS_REGEX = "#[A-Z0-9._%#@=+-]+$DOMAIN_REGEX" private val PATTERN_CONTAIN_MATRIX_ALIAS = MATRIX_ROOM_ALIAS_REGEX.toRegex(RegexOption.IGNORE_CASE) @@ -48,11 +54,11 @@ object MatrixPatterns { private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = MATRIX_EVENT_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) // regex pattern to find message ids in a string. - private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+" + private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$$BASE_64_ALPHABET" private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = MATRIX_EVENT_IDENTIFIER_V3_REGEX.toRegex(RegexOption.IGNORE_CASE) // Ref: https://matrix.org/docs/spec/rooms/v4#event-ids - private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$[A-Z0-9\\-_]+" + private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$$BASE_64_URL_SAFE_ALPHABET" private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = MATRIX_EVENT_IDENTIFIER_V4_REGEX.toRegex(RegexOption.IGNORE_CASE) // regex pattern to find group ids in a string. @@ -76,7 +82,10 @@ object MatrixPatterns { PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER, PATTERN_CONTAIN_MATRIX_ALIAS, PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER, + PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER_DOMAINLESS, PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER, + PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3, + PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4, PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER ) @@ -97,7 +106,9 @@ object MatrixPatterns { * @return true if the string is a valid room Id */ fun isRoomId(str: String?): Boolean { - return str != null && str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER + return str != null && + (str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER || + str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER_DOMAINLESS) } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/SenderNotificationPermissionCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/SenderNotificationPermissionCondition.kt index 82f5023c2f..c3e9b9f527 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/SenderNotificationPermissionCondition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushrules/SenderNotificationPermissionCondition.kt @@ -16,8 +16,7 @@ package org.matrix.android.sdk.api.session.pushrules import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels class SenderNotificationPermissionCondition( /** @@ -35,8 +34,7 @@ class SenderNotificationPermissionCondition( override fun technicalDescription() = "User power level <$key>" - fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean { - val powerLevelsHelper = PowerLevelsHelper(powerLevels) - return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevels.notificationLevel(key) + fun isSatisfied(event: Event, roomPowerLevels: RoomPowerLevels): Boolean { + return event.senderId != null && roomPowerLevels.isUserAbleToTriggerNotification(event.senderId, key) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt index b30c60554f..6da0f8df52 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room import org.matrix.android.sdk.api.query.QueryStateEventValue import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent /** @@ -34,3 +35,10 @@ fun Room.getTimelineEvent(eventId: String): TimelineEvent? = */ fun Room.getStateEvent(eventType: String, stateKey: QueryStateEventValue): Event? = stateService().getStateEvent(eventType, stateKey) + +/** + * Get the current RoomPowerLevels of the room. + */ +fun Room.getRoomPowerLevels(): RoomPowerLevels { + return stateService().getRoomPowerLevels() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt index 0329828130..890b7d64b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt @@ -18,7 +18,8 @@ package org.matrix.android.sdk.api.session.room.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent.Companion.NOTIFICATIONS_ROOM_KEY +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel /** * Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content. @@ -34,7 +35,7 @@ data class PowerLevelsContent( */ @Json(name = "kick") val kick: Int? = null, /** - * The level required to invite a user. Defaults to 50 if unspecified. + * The level required to invite a user. Defaults to 0 if unspecified. */ @Json(name = "invite") val invite: Int? = null, /** @@ -88,7 +89,7 @@ data class PowerLevelsContent( * Get the notification level for a dedicated key. * * @param key the notification key - * @return the level, default to Moderator if the key is not found + * @return the level */ fun notificationLevel(key: String): Int { return when (val value = notifications.orEmpty()[key]) { @@ -96,10 +97,9 @@ data class PowerLevelsContent( is String -> value.toInt() is Double -> value.toInt() is Int -> value - else -> Role.Moderator.value + else -> defaultNotificationLevel(key) } } - companion object { /** * Key to use for content.notifications and get the level required to trigger an @room notification. Defaults to 50 if unspecified. @@ -108,11 +108,20 @@ data class PowerLevelsContent( } } +private fun defaultNotificationLevel(key: String): Int { + return when (key) { + NOTIFICATIONS_ROOM_KEY -> UserPowerLevel.Moderator.value + else -> UserPowerLevel.User.value + } +} + // Fallback to default value, defined in the Matrix specification -fun PowerLevelsContent.banOrDefault() = ban ?: Role.Moderator.value -fun PowerLevelsContent.kickOrDefault() = kick ?: Role.Moderator.value -fun PowerLevelsContent.inviteOrDefault() = invite ?: Role.Moderator.value -fun PowerLevelsContent.redactOrDefault() = redact ?: Role.Moderator.value -fun PowerLevelsContent.eventsDefaultOrDefault() = eventsDefault ?: Role.Default.value -fun PowerLevelsContent.usersDefaultOrDefault() = usersDefault ?: Role.Default.value -fun PowerLevelsContent.stateDefaultOrDefault() = stateDefault ?: Role.Moderator.value +fun PowerLevelsContent?.banOrDefault() = this?.ban ?: UserPowerLevel.Moderator.value +fun PowerLevelsContent?.kickOrDefault() = this?.kick ?: UserPowerLevel.Moderator.value +fun PowerLevelsContent?.inviteOrDefault() = this?.invite ?: UserPowerLevel.User.value +fun PowerLevelsContent?.redactOrDefault() = this?.redact ?: UserPowerLevel.Moderator.value +fun PowerLevelsContent?.eventsDefaultOrDefault() = this?.eventsDefault ?: UserPowerLevel.User.value +fun PowerLevelsContent?.usersDefaultOrDefault() = this?.usersDefault ?: UserPowerLevel.User.value +fun PowerLevelsContent?.stateDefaultOrDefault() = this?.stateDefault ?: UserPowerLevel.Moderator.value + +fun PowerLevelsContent?.notificationLevelOrDefault(key: String) = this?.notificationLevel(key) ?: defaultNotificationLevel(key) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt index d73c941a86..2408f4a004 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt @@ -18,15 +18,39 @@ package org.matrix.android.sdk.api.session.room.model.create import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel /** * Content of a m.room.create type event. */ @JsonClass(generateAdapter = true) data class RoomCreateContent( + // Creator should be replaced by the sender of the event @Json(name = "creator") val creator: String? = null, @Json(name = "room_version") val roomVersion: String? = null, @Json(name = "predecessor") val predecessor: Predecessor? = null, // Defines the room type, see #RoomType (user extensible) - @Json(name = "type") val type: String? = null + @Json(name = "type") val type: String? = null, + @Json(name = "additional_creators") val additionalCreators: List? = null, ) + +data class RoomCreateContentWithSender( + val senderId: String, + val inner: RoomCreateContent +) { + val creators = setOf(senderId) + inner.additionalCreators.orEmpty().toSet() +} + +fun Event.getRoomCreateContentWithSender(): RoomCreateContentWithSender? { + if (this.type != EventType.STATE_ROOM_CREATE) return null + val innerContent = getClearContent().toModel() ?: return null + val senderId = senderId ?: return null + return RoomCreateContentWithSender(senderId, innerContent) +} + +fun RoomCreateContent.explicitlyPrivilegeRoomCreators(): Boolean { + val supportedRoomVersions = listOf("org.matrix.hydra.11", "12") + return supportedRoomVersions.contains(roomVersion) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/Role.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/Role.kt index c5cc573458..dce48c8069 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/Role.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/Role.kt @@ -17,26 +17,21 @@ package org.matrix.android.sdk.api.session.room.powerlevels -sealed class Role(open val value: Int) : Comparable { - object Admin : Role(100) - object Moderator : Role(50) - object Default : Role(0) - data class Custom(override val value: Int) : Role(value) - - override fun compareTo(other: Role): Int { - return value.compareTo(other.value) - } +enum class Role { + Creator, + SuperAdmin, + Admin, + Moderator, + User; companion object { - - // Order matters, default value should be checked after defined roles - fun fromValue(value: Int, default: Int): Role { - return when (value) { - Admin.value -> Admin - Moderator.value -> Moderator - Default.value, - default -> Default - else -> Custom(value) + fun getSuggestedRole(userPowerLevel: UserPowerLevel): Role { + return when { + userPowerLevel == UserPowerLevel.Infinite -> Creator + userPowerLevel >= UserPowerLevel.SuperAdmin -> SuperAdmin + userPowerLevel >= UserPowerLevel.Admin -> Admin + userPowerLevel >= UserPowerLevel.Moderator -> Moderator + else -> User } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/RoomPowerLevels.kt similarity index 58% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/RoomPowerLevels.kt index 36993074aa..0d66136730 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/RoomPowerLevels.kt @@ -19,17 +19,23 @@ package org.matrix.android.sdk.api.session.room.powerlevels import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.banOrDefault +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContentWithSender +import org.matrix.android.sdk.api.session.room.model.create.explicitlyPrivilegeRoomCreators import org.matrix.android.sdk.api.session.room.model.eventsDefaultOrDefault import org.matrix.android.sdk.api.session.room.model.inviteOrDefault import org.matrix.android.sdk.api.session.room.model.kickOrDefault +import org.matrix.android.sdk.api.session.room.model.notificationLevelOrDefault import org.matrix.android.sdk.api.session.room.model.redactOrDefault import org.matrix.android.sdk.api.session.room.model.stateDefaultOrDefault import org.matrix.android.sdk.api.session.room.model.usersDefaultOrDefault /** - * This class is an helper around PowerLevelsContent. + * This class is an helper around PowerLevelsContent and RoomCreateContent. */ -class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { +class RoomPowerLevels( + val powerLevelsContent: PowerLevelsContent?, + private val roomCreateContent: RoomCreateContentWithSender?, +) { /** * Returns the user power level of a dedicated user Id. @@ -37,10 +43,14 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { * @param userId the user id * @return the power level */ - fun getUserPowerLevelValue(userId: String): Int { - return powerLevelsContent.users + fun getUserPowerLevel(userId: String): UserPowerLevel { + if (shouldGiveInfinitePowerLevel(userId)) return UserPowerLevel.Infinite + if (powerLevelsContent == null) return UserPowerLevel.User + val value = powerLevelsContent.users ?.get(userId) ?: powerLevelsContent.usersDefaultOrDefault() + + return UserPowerLevel.Value(value) } /** @@ -49,10 +59,9 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { * @param userId the user id * @return the power level */ - fun getUserRole(userId: String): Role { - val value = getUserPowerLevelValue(userId) - // I think we should use powerLevelsContent.usersDefault, but Ganfra told me that it was like that on riot-Web - return Role.fromValue(value, powerLevelsContent.eventsDefaultOrDefault()) + fun getSuggestedRole(userId: String): Role { + val value = getUserPowerLevel(userId) + return Role.getSuggestedRole(value) } /** @@ -65,14 +74,14 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { */ fun isUserAllowedToSend(userId: String, isState: Boolean, eventType: String?): Boolean { return if (userId.isNotEmpty()) { - val powerLevel = getUserPowerLevelValue(userId) - val minimumPowerLevel = powerLevelsContent.events?.get(eventType) + val powerLevel = getUserPowerLevel(userId) + val minimumPowerLevel = powerLevelsContent?.events?.get(eventType) ?: if (isState) { powerLevelsContent.stateDefaultOrDefault() } else { powerLevelsContent.eventsDefaultOrDefault() } - powerLevel >= minimumPowerLevel + powerLevel >= UserPowerLevel.Value(minimumPowerLevel) } else false } @@ -82,8 +91,8 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { * @return true if able to invite */ fun isUserAbleToInvite(userId: String): Boolean { - val powerLevel = getUserPowerLevelValue(userId) - return powerLevel >= powerLevelsContent.inviteOrDefault() + val powerLevel = getUserPowerLevel(userId) + return powerLevel >= UserPowerLevel.Value(powerLevelsContent.inviteOrDefault()) } /** @@ -92,8 +101,8 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { * @return true if able to ban */ fun isUserAbleToBan(userId: String): Boolean { - val powerLevel = getUserPowerLevelValue(userId) - return powerLevel >= powerLevelsContent.banOrDefault() + val powerLevel = getUserPowerLevel(userId) + return powerLevel >= UserPowerLevel.Value(powerLevelsContent.banOrDefault()) } /** @@ -102,8 +111,8 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { * @return true if able to kick */ fun isUserAbleToKick(userId: String): Boolean { - val powerLevel = getUserPowerLevelValue(userId) - return powerLevel >= powerLevelsContent.kickOrDefault() + val powerLevel = getUserPowerLevel(userId) + return powerLevel >= UserPowerLevel.Value(powerLevelsContent.kickOrDefault()) } /** @@ -112,7 +121,22 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { * @return true if able to redact */ fun isUserAbleToRedact(userId: String): Boolean { - val powerLevel = getUserPowerLevelValue(userId) - return powerLevel >= powerLevelsContent.redactOrDefault() + val powerLevel = getUserPowerLevel(userId) + return powerLevel >= UserPowerLevel.Value(powerLevelsContent.redactOrDefault()) + } + + fun isUserAbleToTriggerNotification(userId: String, notificationKey: String): Boolean { + val userPowerLevel = getUserPowerLevel(userId) + val notificationPowerLevel = UserPowerLevel.Value(powerLevelsContent.notificationLevelOrDefault(key = notificationKey)) + return userPowerLevel >= notificationPowerLevel + } + + private fun shouldGiveInfinitePowerLevel(userId: String): Boolean { + if (roomCreateContent == null) return false + return if (roomCreateContent.inner.explicitlyPrivilegeRoomCreators()) { + roomCreateContent.creators.contains(userId) + } else { + false + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/UserPowerLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/UserPowerLevel.kt new file mode 100644 index 0000000000..7d213cb269 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/UserPowerLevel.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2025 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 org.matrix.android.sdk.api.session.room.powerlevels + +sealed interface UserPowerLevel : Comparable { + data object Infinite : UserPowerLevel + + @JvmInline + value class Value(val value: Int) : UserPowerLevel + + override fun compareTo(other: UserPowerLevel): Int { + return when (this) { + Infinite -> when (other) { + Infinite -> 0 + is Value -> 1 + } + is Value -> when (other) { + Infinite -> -1 + is Value -> value.compareTo(other.value) + } + } + } + + companion object { + val User = Value(0) + val Moderator = Value(50) + val Admin = Value(100) + val SuperAdmin = Value(150) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index 6ca63c2c49..6e01d7edfa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.Optional @@ -106,4 +107,6 @@ interface StateService { suspend fun setJoinRulePublic() suspend fun setJoinRuleInviteOnly() suspend fun setJoinRuleRestricted(allowList: List) + fun getRoomPowerLevels(): RoomPowerLevels + fun getRoomPowerLevelsLive(): LiveData } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index a3c68c2230..bbc4c3bb39 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -95,7 +95,7 @@ import org.matrix.rustcomponents.sdk.crypto.ProgressListener as RustProgressList class CryptoLogger : Logger { override fun log(logLine: String) { - Timber.d(logLine) + Timber.d(logLine.trimEnd()) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt index 334a8c5076..f11b8a031c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -38,7 +38,8 @@ internal class FormattedJsonHttpLogger( */ @Synchronized override fun log(message: String) { - Timber.v(message) + Timber.v(message.take(20_000)) + if (message.length > 20_000) return // Try to log formatted Json only if there is a chance that [message] contains Json. // It can be only the case if we log the bodies of Http requests. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt index 5fb20bb259..7f1277864c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt @@ -17,15 +17,11 @@ package org.matrix.android.sdk.internal.session.permalinks import org.matrix.android.sdk.api.MatrixPatterns.getServerName -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.RoomGetter +import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import java.net.URLEncoder import javax.inject.Inject @@ -101,10 +97,7 @@ internal class ViaParameterFinder @Inject constructor( } fun userCanInvite(userId: String, roomId: String): Boolean { - val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content?.toModel() - ?.let { PowerLevelsHelper(it) } - - return powerLevelsHelper?.isUserAbleToInvite(userId) ?: false + val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(roomId) + return roomPowerLevels.isUserAbleToInvite(userId) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt index c2310f4fda..3a7a43c8ca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt @@ -15,24 +15,20 @@ */ package org.matrix.android.sdk.internal.session.pushers -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.pushrules.ConditionResolver import org.matrix.android.sdk.api.session.pushrules.ContainsDisplayNameCondition import org.matrix.android.sdk.api.session.pushrules.EventMatchCondition import org.matrix.android.sdk.api.session.pushrules.RoomMemberCountCondition import org.matrix.android.sdk.api.session.pushrules.SenderNotificationPermissionCondition -import org.matrix.android.sdk.api.session.room.getStateEvent -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.getRoomPowerLevels import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.RoomGetter import javax.inject.Inject internal class DefaultConditionResolver @Inject constructor( private val roomGetter: RoomGetter, - @UserId private val userId: String + @UserId private val userId: String, ) : ConditionResolver { override fun resolveEventMatchCondition( @@ -55,13 +51,8 @@ internal class DefaultConditionResolver @Inject constructor( ): Boolean { val roomId = event.roomId ?: return false val room = roomGetter.getRoom(roomId) ?: return false - - val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - ?.toModel() - ?: PowerLevelsContent() - - return condition.isSatisfied(event, powerLevelsContent) + val roomPowerLevels = room.getRoomPowerLevels() + return condition.isSatisfied(event, roomPowerLevels) } override fun resolveContainsDisplayNameCondition( 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 edc10bd187..821aeaa494 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 @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.session.room import io.realm.Realm -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.crypto.verification.VerificationState import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation import org.matrix.android.sdk.api.session.events.model.Event @@ -27,7 +26,6 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent @@ -36,7 +34,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.crypto.verification.toState import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent @@ -62,6 +59,7 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor +import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber @@ -216,9 +214,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } in EventType.POLL_END.values -> { sessionManager.getSessionComponent(sessionId)?.session()?.let { session -> - getPowerLevelsHelper(event.roomId)?.let { - pollAggregationProcessor.handlePollEndEvent(session, it, realm, event) - } + val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(event.roomId) + pollAggregationProcessor.handlePollEndEvent(session, roomPowerLevels, realm, event) } } in EventType.STATE_ROOM_BEACON_INFO.values -> { @@ -381,12 +378,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } - private fun getPowerLevelsHelper(roomId: String): PowerLevelsHelper? { - return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content?.toModel() - ?.let { PowerLevelsHelper(it) } - } - private fun handleInitialAggregatedRelations( realm: Realm, event: Event, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index ca224cd543..5a6648934e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -32,7 +32,7 @@ import org.matrix.android.sdk.api.session.room.model.VoteInfo import org.matrix.android.sdk.api.session.room.model.VoteSummary import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.internal.database.mapper.ContentMapper @@ -160,13 +160,13 @@ internal class DefaultPollAggregationProcessor @Inject constructor( return true } - override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean { + override fun handlePollEndEvent(session: Session, roomPowerLevels: RoomPowerLevels, realm: Realm, event: Event): Boolean { val roomId = event.roomId ?: return false val pollEventId = event.getRelationContent()?.eventId ?: return false val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId val isPollOwner = pollOwnerId == event.senderId - if (!isPollOwner && !powerLevelsHelper.isUserAbleToRedact(event.senderId ?: "")) { + if (!isPollOwner && !roomPowerLevels.isUserAbleToRedact(event.senderId ?: "")) { return false } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt index 33a69b720a..578ba6064e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll import io.realm.Realm import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels internal interface PollAggregationProcessor { /** @@ -48,7 +48,7 @@ internal interface PollAggregationProcessor { */ fun handlePollEndEvent( session: Session, - powerLevelsHelper: PowerLevelsHelper, + roomPowerLevels: RoomPowerLevels, realm: Realm, event: Event ): Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/powerlevels/RoomPowerLevels.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/powerlevels/RoomPowerLevels.kt new file mode 100644 index 0000000000..a72a0951f2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/powerlevels/RoomPowerLevels.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2025 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 org.matrix.android.sdk.internal.session.room.powerlevels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.model.create.getRoomCreateContentWithSender +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource + +internal fun StateEventDataSource.getRoomPowerLevels(roomId: String): RoomPowerLevels { + val powerLevelsEvent = getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + val roomCreateEvent = getStateEvent(roomId, EventType.STATE_ROOM_CREATE, QueryStringValue.IsEmpty) + return createRoomPowerLevels( + powerLevelsEvent = powerLevelsEvent, + roomCreateEvent = roomCreateEvent + ) +} + +internal fun StateEventDataSource.getRoomPowerLevelsLive(roomId: String): LiveData { + val powerLevelsEventLive = getStateEventLive(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + val roomCreateEventLive = getStateEventLive(roomId, EventType.STATE_ROOM_CREATE, QueryStringValue.IsEmpty) + val resultLiveData = MediatorLiveData() + + fun emitIfReady(powerLevelEvent: Optional?, roomCreateEvent: Optional?) { + if (powerLevelEvent != null && roomCreateEvent != null) { + val roomPowerLevels = createRoomPowerLevels( + powerLevelsEvent = powerLevelEvent.getOrNull(), + roomCreateEvent = roomCreateEvent.getOrNull() + ) + resultLiveData.postValue(roomPowerLevels) + } + } + resultLiveData.apply { + var powerLevelEvent: Optional? = null + var roomCreateEvent: Optional? = null + + addSource(powerLevelsEventLive) { + powerLevelEvent = it + emitIfReady(powerLevelEvent, roomCreateEvent) + } + addSource(roomCreateEventLive) { + roomCreateEvent = it + emitIfReady(powerLevelEvent, roomCreateEvent) + } + } + return resultLiveData +} + +private fun createRoomPowerLevels(powerLevelsEvent: Event?, roomCreateEvent: Event?): RoomPowerLevels { + val powerLevelsContent = powerLevelsEvent?.content?.toModel() + val roomCreateContent = roomCreateEvent?.getRoomCreateContentWithSender() + return RoomPowerLevels(powerLevelsContent, roomCreateContent) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index ad47b82428..b939490ae3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -31,11 +31,14 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.content.FileUploader +import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels +import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevelsLive internal class DefaultStateService @AssistedInject constructor( @Assisted private val roomId: String, @@ -65,6 +68,14 @@ internal class DefaultStateService @AssistedInject constructor( return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey) } + override fun getRoomPowerLevels(): RoomPowerLevels { + return stateEventDataSource.getRoomPowerLevels(roomId) + } + + override fun getRoomPowerLevelsLive(): LiveData { + return stateEventDataSource.getRoomPowerLevelsLive(roomId) + } + override suspend fun sendStateEvent( eventType: String, stateKey: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index cbb75398c4..8820af2034 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -34,7 +34,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomTopicContent import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContentWithSender +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications @@ -313,13 +314,25 @@ internal class RoomSummaryUpdater @Inject constructor( // check if sender can post child relation in parent? val senderId = parentInfo.stateEventSender val parentRoomId = parentInfo.roomId - val powerLevelsHelper = CurrentStateEventEntity + val powerLevelsContent = CurrentStateEventEntity .getOrNull(realm, parentRoomId, "", EventType.STATE_ROOM_POWER_LEVELS) ?.root ?.let { ContentMapper.map(it.content).toModel() } - ?.let { PowerLevelsHelper(it) } - isValidRelation = powerLevelsHelper?.isUserAllowedToSend(senderId, true, EventType.STATE_SPACE_CHILD) ?: false + val roomCreateContent = CurrentStateEventEntity + .getOrNull(realm, parentRoomId, "", EventType.STATE_ROOM_CREATE) + ?.root + ?.let { + val content = ContentMapper.map(it.content).toModel() + val sender = it.sender + if (content != null && sender != null) { + RoomCreateContentWithSender(sender, content) + } else { + null + } + } + val roomPowerLevels = RoomPowerLevels(powerLevelsContent, roomCreateContent) + isValidRelation = roomPowerLevels.isUserAllowedToSend(senderId, true, EventType.STATE_SPACE_CHILD) } if (isValidRelation) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt index 0bde3a11d2..0fec0e4c17 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt @@ -23,11 +23,10 @@ import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.version.RoomVersionService import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource +import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource internal class DefaultRoomVersionService @AssistedInject constructor( @@ -71,11 +70,8 @@ internal class DefaultRoomVersionService @AssistedInject constructor( } override fun userMayUpgradeRoom(userId: String): Boolean { - val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content?.toModel() - ?.let { PowerLevelsHelper(it) } - - return powerLevelsHelper?.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_TOMBSTONE) ?: false + val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(roomId) + return roomPowerLevels.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_TOMBSTONE) } companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index cd13b03017..9b6e87a736 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -35,8 +35,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper -import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel import org.matrix.android.sdk.api.session.space.CreateSpaceParams import org.matrix.android.sdk.api.session.space.JoinSpaceResult import org.matrix.android.sdk.api.session.space.Space @@ -47,11 +46,13 @@ import org.matrix.android.sdk.api.session.space.model.SpaceChildContent import org.matrix.android.sdk.api.session.space.model.SpaceChildSummaryEvent import org.matrix.android.sdk.api.session.space.model.SpaceParentContent import org.matrix.android.sdk.api.session.space.peeking.SpacePeekResult +import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.RoomGetter import org.matrix.android.sdk.internal.session.room.SpaceGetter import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask +import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask @@ -83,7 +84,7 @@ internal class DefaultSpaceService @Inject constructor( if (isPublic) { this.roomAliasName = roomAliasLocalPart this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy( - invite = if (isPublic) Role.Default.value else Role.Moderator.value + invite = UserPowerLevel.User.value ) this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE @@ -253,15 +254,8 @@ internal class DefaultSpaceService @Inject constructor( if (roomSummaryDataSource.getRoomSummary(parentSpaceId)?.membership != Membership.JOIN) { throw UnsupportedOperationException("Cannot add canonical child if not member of parent") } - val powerLevelsEvent = stateEventDataSource.getStateEvent( - roomId = parentSpaceId, - eventType = EventType.STATE_ROOM_POWER_LEVELS, - stateKey = QueryStringValue.IsEmpty - ) - val powerLevelsContent = powerLevelsEvent?.content?.toModel() - ?: throw UnsupportedOperationException("Cannot add canonical child, missing power level") - val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) - if (!powerLevelsHelper.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) { + val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(parentSpaceId) + if (!roomPowerLevels.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) { throw UnsupportedOperationException("Cannot add canonical child, not enough power level") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt index 7359fdbd91..8f197706ff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt @@ -30,15 +30,13 @@ import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager +import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory @@ -200,12 +198,7 @@ internal class WidgetManager @Inject constructor( } fun hasPermissionsToHandleWidgets(roomId: String): Boolean { - val powerLevelsEvent = stateEventDataSource.getStateEvent( - roomId = roomId, - eventType = EventType.STATE_ROOM_POWER_LEVELS, - stateKey = QueryStringValue.IsEmpty - ) - val powerLevelsContent = powerLevelsEvent?.content?.toModel() ?: return false - return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY) + val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(roomId) + return roomPowerLevels.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt index 248c4b322d..bdfffa9d51 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt @@ -33,7 +33,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.getTimelineEvent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity @@ -255,9 +255,9 @@ class DefaultPollAggregationProcessorTest { every { room.getTimelineEvent(eventId) } returns if (hasExistingTimelineEvent) A_TIMELINE_EVENT else null } - private fun mockRedactionPowerLevels(userId: String, isAbleToRedact: Boolean): PowerLevelsHelper { - val powerLevelsHelper = mockk() - every { powerLevelsHelper.isUserAbleToRedact(userId) } returns isAbleToRedact - return powerLevelsHelper + private fun mockRedactionPowerLevels(userId: String, isAbleToRedact: Boolean): RoomPowerLevels { + val roomPowerLevels = mockk() + every { roomPowerLevels.isUserAbleToRedact(userId) } returns isAbleToRedact + return roomPowerLevels } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateLocalRoomStateEventsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateLocalRoomStateEventsTaskTest.kt index 1c2cf293b6..b05a890c1b 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateLocalRoomStateEventsTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateLocalRoomStateEventsTaskTest.kt @@ -51,7 +51,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent import org.matrix.android.sdk.api.session.room.model.RoomTopicContent import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent -import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel import org.matrix.android.sdk.api.session.user.UserService import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier.Companion.MEDIUM_EMAIL @@ -372,13 +372,13 @@ internal class DefaultCreateLocalRoomStateEventsTaskTest { // Power levels val powerLevelsContent = result.find { it.type == EventType.STATE_ROOM_POWER_LEVELS }?.content.toModel() powerLevelsContent.shouldNotBeNull() - powerLevelsContent.ban shouldBeEqualTo Role.Moderator.value - powerLevelsContent.kick shouldBeEqualTo Role.Moderator.value - powerLevelsContent.invite shouldBeEqualTo Role.Moderator.value - powerLevelsContent.redact shouldBeEqualTo Role.Moderator.value - powerLevelsContent.eventsDefault shouldBeEqualTo Role.Default.value - powerLevelsContent.usersDefault shouldBeEqualTo Role.Default.value - powerLevelsContent.stateDefault shouldBeEqualTo Role.Moderator.value + powerLevelsContent.ban shouldBeEqualTo UserPowerLevel.Moderator.value + powerLevelsContent.kick shouldBeEqualTo UserPowerLevel.Moderator.value + powerLevelsContent.invite shouldBeEqualTo UserPowerLevel.User.value + powerLevelsContent.redact shouldBeEqualTo UserPowerLevel.Moderator.value + powerLevelsContent.eventsDefault shouldBeEqualTo UserPowerLevel.User.value + powerLevelsContent.usersDefault shouldBeEqualTo UserPowerLevel.User.value + powerLevelsContent.stateDefault shouldBeEqualTo UserPowerLevel.Moderator.value // Guest access result.find { it.type == EventType.STATE_ROOM_GUEST_ACCESS } ?.content.toModel()?.guestAccess shouldBeEqualTo GuestAccess.Forbidden diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 895924a712..4faeae495d 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 = 42 +ext.versionPatch = 44 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt index 288b7a89bd..876baf69d9 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/DebugMenuActivity.kt @@ -12,6 +12,7 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.content.Intent import android.os.Build +import android.view.View import androidx.core.app.NotificationCompat import androidx.core.app.Person import androidx.core.content.getSystemService @@ -49,7 +50,9 @@ import javax.inject.Inject class DebugMenuActivity : VectorBaseActivity() { override fun getBinding() = ActivityDebugMenuBinding.inflate(layoutInflater) - + override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout @Inject lateinit var clock: Clock private lateinit var buffer: ByteArray diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt index cae9f63b22..9f5622713b 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/DebugPermissionActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.debug import android.Manifest import android.content.pm.PackageManager import android.os.Build +import android.view.View import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -32,6 +33,9 @@ class DebugPermissionActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { if (isFirstCreation()) { addFragment( diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.kt index 2f7ff692af..b4a7793962 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesSettingsActivity.kt @@ -8,6 +8,7 @@ package im.vector.app.features.debug.features import android.os.Bundle +import android.view.View import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith @@ -24,6 +25,9 @@ class DebugFeaturesSettingsActivity : VectorBaseActivity() { override fun getBinding() = ActivityDebugJitsiBinding.inflate(layoutInflater) override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout @SuppressLint("SetTextI18n") override fun initUiAndData() { diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksActivity.kt index f942fc342d..7eb9ea99d6 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksActivity.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/leak/DebugMemoryLeaksActivity.kt @@ -7,6 +7,7 @@ package im.vector.app.features.debug.leak +import android.view.View import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.VectorBaseActivity @@ -17,6 +18,10 @@ class DebugMemoryLeaksActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { if (isFirstCreation()) { addFragment( diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsActivity.kt b/vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsActivity.kt index 017bde5eb7..2a539a21ca 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsActivity.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsActivity.kt @@ -7,6 +7,7 @@ package im.vector.app.features.debug.settings +import android.view.View import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.VectorBaseActivity @@ -17,6 +18,10 @@ class DebugPrivateSettingsActivity : VectorBaseActivity() override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { if (isFirstCreation()) { addFragment( 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 50bced59ed..84b87b1404 100644 --- a/vector-app/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector-app/src/main/java/im/vector/app/VectorApplication.kt @@ -161,6 +161,7 @@ class VectorApplication : "Noto Color Emoji Compat", R.array.com_google_android_gms_fonts_certs ) + @Suppress("DEPRECATION") FontsContractCompat.requestFont(this, fontRequest, emojiCompatFontProvider, getFontThreadHandler()) vectorLocale.init() ThemeUtils.init(this) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index fab5be1257..7455c0847f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -167,8 +167,7 @@ + android:parentActivityName=".features.home.HomeActivity"> @@ -369,8 +368,8 @@ + android:exported="false" + android:foregroundServiceType="phoneCall"> @@ -387,7 +386,8 @@ + android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" + tools:targetApi="M"> @@ -413,8 +413,7 @@ android:name=".features.call.audio.MicrophoneAccessService" android:exported="false" android:foregroundServiceType="microphone" - android:permission="android.permission.FOREGROUND_SERVICE_MICROPHONE"> - + android:permission="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> diff --git a/vector/src/main/java/im/vector/app/SpaceStateHandlerImpl.kt b/vector/src/main/java/im/vector/app/SpaceStateHandlerImpl.kt index 97b6eb6209..06abc16cbf 100644 --- a/vector/src/main/java/im/vector/app/SpaceStateHandlerImpl.kt +++ b/vector/src/main/java/im/vector/app/SpaceStateHandlerImpl.kt @@ -137,7 +137,7 @@ class SpaceStateHandlerImpl @Inject constructor( override fun popSpaceBackstack(): String? { vectorPreferences.getSpaceBackstack().toMutableList().apply { - val poppedSpaceId = removeLast() + val poppedSpaceId = removeAt(lastIndex) vectorPreferences.setSpaceBackstack(this) return poppedSpaceId } diff --git a/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt index 06eecf1d41..12ed8418bc 100644 --- a/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/SimpleFragmentActivity.kt @@ -6,6 +6,7 @@ */ package im.vector.app.core.platform +import android.view.View import androidx.core.view.isGone import androidx.core.view.isVisible import im.vector.app.core.extensions.hideKeyboard @@ -20,6 +21,9 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { final override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { setupToolbar(views.toolbar) .allowBack(true) 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 355a164ab0..bf621b60a3 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 @@ -10,25 +10,27 @@ package im.vector.app.core.platform import android.annotation.SuppressLint import android.app.Activity import android.content.Context -import android.os.Build import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import android.view.WindowInsetsController import android.view.WindowManager import android.widget.TextView +import androidx.activity.enableEdgeToEdge import androidx.annotation.CallSuper import androidx.annotation.MainThread import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.app.MultiWindowModeChangedInfo -import androidx.core.content.ContextCompat import androidx.core.util.Consumer import androidx.core.view.MenuProvider +import androidx.core.view.ViewCompat +import androidx.core.view.ViewGroupCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider @@ -208,6 +210,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver val activityEntryPoint = EntryPointAccessors.fromActivity(this, ActivityEntryPoint::class.java) ThemeUtils.setActivityTheme(this, getOtherThemes()) viewModelFactory = activityEntryPoint.viewModelFactory() + enableEdgeToEdge() + ViewGroupCompat.installCompatInsetsDispatch(window.decorView) super.onCreate(savedInstanceState) addOnMultiWindowModeChangedListener(onMultiWindowModeChangedListener) setupMenu() @@ -247,7 +251,9 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver if (vectorPreferences.isNewAppLayoutEnabled()) { tryOrNull { // Add to XML theme when feature flag is removed val toolbarBackground = MaterialColors.getColor(views.root, im.vector.lib.ui.styles.R.attr.vctr_toolbar_background) + @Suppress("DEPRECATION") window.statusBarColor = toolbarBackground + @Suppress("DEPRECATION") window.navigationBarColor = toolbarBackground } } @@ -334,7 +340,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver private fun handleCertificateError(certificateError: GlobalError.CertificateError) { singletonEntryPoint() .unrecognizedCertificateDialog() - .show(this, + .show( + this, certificateError.fingerprint, object : UnrecognizedCertificateDialog.Callback { override fun onAccept() { @@ -411,6 +418,21 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver // Just log that a change occurred. Timber.w("MDM data has been updated") } + + ViewCompat.setOnApplyWindowInsetsListener(rootView) { v, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() or + WindowInsetsCompat.Type.ime() + ) + v.updatePadding( + systemBars.left, + systemBars.top, + systemBars.right, + systemBars.bottom, + ) + WindowInsetsCompat.CONSUMED + } } private val postResumeScheduledActions = mutableListOf<() -> Unit>() @@ -444,14 +466,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver mdmService.unregisterListener(this) } - override fun onWindowFocusChanged(hasFocus: Boolean) { - super.onWindowFocusChanged(hasFocus) - - if (hasFocus && displayInFullscreen()) { - setFullScreen() - } - } - private val onMultiWindowModeChangedListener = Consumer { Timber.w("onMultiWindowModeChanged. isInMultiWindowMode: ${it.isInMultiWindowMode}") bugReporter.inMultiWindowMode = it.isInMultiWindowMode @@ -461,30 +475,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver * PRIVATE METHODS * ========================================================================================== */ - /** - * Force to render the activity in fullscreen. - */ - private fun setFullScreen() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - window.setDecorFitsSystemWindows(false) - // New API instead of SYSTEM_UI_FLAG_IMMERSIVE - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - // New API instead of FLAG_TRANSLUCENT_STATUS - window.statusBarColor = ContextCompat.getColor(this, im.vector.lib.ui.styles.R.color.half_transparent_status_bar) - // New API instead of FLAG_TRANSLUCENT_NAVIGATION - window.navigationBarColor = ContextCompat.getColor(this, im.vector.lib.ui.styles.R.color.half_transparent_status_bar) - } else { - @Suppress("DEPRECATION") - window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_FULLSCREEN - or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) - } - } - private fun handleMenuItemHome(item: MenuItem): Boolean { return when (item.itemId) { android.R.id.home -> { @@ -586,8 +576,6 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver abstract fun getBinding(): VB - open fun displayInFullscreen() = false - open fun doBeforeSetContentView() = Unit open fun initUiAndData() = Unit @@ -626,6 +614,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver open fun getCoordinatorLayout(): CoordinatorLayout? = null + abstract val rootView: View + /* ========================================================================================== * User Consent * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/app/core/services/BluetoothHeadsetReceiver.kt b/vector/src/main/java/im/vector/app/core/services/BluetoothHeadsetReceiver.kt index dd04e5664e..031ea2268d 100644 --- a/vector/src/main/java/im/vector/app/core/services/BluetoothHeadsetReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/services/BluetoothHeadsetReceiver.kt @@ -18,6 +18,10 @@ import androidx.core.content.ContextCompat import im.vector.lib.core.utils.compat.getParcelableExtraCompat import java.lang.ref.WeakReference +/** + * It's only used in API 21 and 22 so we will not have security exception on these OS, + * so it's safe to use @Suppress("MissingPermission"). + */ class BluetoothHeadsetReceiver : BroadcastReceiver() { interface EventListener { @@ -53,12 +57,15 @@ class BluetoothHeadsetReceiver : BroadcastReceiver() { } val device = intent.getParcelableExtraCompat(BluetoothDevice.EXTRA_DEVICE) + @Suppress("MissingPermission") val deviceName = device?.name + @Suppress("MissingPermission") when (device?.bluetoothClass?.deviceClass) { BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE, BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO, BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET -> { // filter only device that we care about for + @Suppress("MissingPermission") delegate?.get()?.onBTHeadsetEvent( BTHeadsetPlugEvent( plugged = headsetConnected, diff --git a/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt b/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt index f818ab412e..9e0a718b15 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt @@ -8,11 +8,14 @@ package im.vector.app.core.services +import android.Manifest import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.os.Binder import android.support.v4.media.session.MediaSessionCompat import android.view.KeyEvent +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.media.session.MediaButtonReceiver @@ -150,7 +153,8 @@ class CallAndroidService : VectorAndroidService() { val isVideoCall = call.mxCall.isVideoCall val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false) Timber.tag(loggerTag.value).v("displayIncomingCallNotification : display the dedicated notification") - val incomingCallAlert = IncomingCallAlert(callId, + val incomingCallAlert = IncomingCallAlert( + callId, shouldBeDisplayedIn = { activity -> if (activity is VectorCallActivity) { activity.intent.getParcelableExtraCompat(Mavericks.KEY_ARG)?.callId != call.callId @@ -176,7 +180,11 @@ class CallAndroidService : VectorAndroidService() { if (knownCalls.isEmpty()) { startForegroundCompat(callId.hashCode(), notification) } else { - notificationManager.notify(callId.hashCode(), notification) + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + Timber.w("Not allowed to notify.") + } else { + notificationManager.notify(callId.hashCode(), notification) + } } knownCalls[callId] = callInformation } @@ -234,7 +242,11 @@ class CallAndroidService : VectorAndroidService() { if (knownCalls.isEmpty()) { startForegroundCompat(callId.hashCode(), notification) } else { - notificationManager.notify(callId.hashCode(), notification) + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + Timber.w("Not allowed to notify.") + } else { + notificationManager.notify(callId.hashCode(), notification) + } } knownCalls[callId] = callInformation } @@ -258,7 +270,11 @@ class CallAndroidService : VectorAndroidService() { if (knownCalls.isEmpty()) { startForegroundCompat(callId.hashCode(), notification) } else { - notificationManager.notify(callId.hashCode(), notification) + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + Timber.w("Not allowed to notify.") + } else { + notificationManager.notify(callId.hashCode(), notification) + } } knownCalls[callId] = callInformation } diff --git a/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt index 7834d6a014..5b84579b2b 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt @@ -658,7 +658,8 @@ class ExpandingBottomSheetBehavior : CoordinatorLayout.Behavior { val insetsType = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime() val imeInsets = insets.getInsets(insetsType) insetTop = imeInsets.top - insetBottom = imeInsets.bottom + // Now that edgeToEdge is enabled, disable the bottom padding. + insetBottom = 0 insetLeft = imeInsets.left insetRight = imeInsets.right diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionChecker.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionChecker.kt new file mode 100644 index 0000000000..1323edf188 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionChecker.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package im.vector.app.core.utils + +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat +import dagger.Binds +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Inject + +interface PermissionChecker { + + @InstallIn(SingletonComponent::class) + @dagger.Module + interface Module { + @Binds + fun bindPermissionChecker(permissionChecker: AndroidPermissionChecker): PermissionChecker + } + + fun checkPermission(vararg permissions: String): Boolean +} + +class AndroidPermissionChecker @Inject constructor( + private val applicationContext: Context, +) : PermissionChecker { + override fun checkPermission(vararg permissions: String): Boolean { + return permissions.any { permission -> + ActivityCompat.checkSelfPermission(applicationContext, permission) != PackageManager.PERMISSION_GRANTED + } + } +} 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 6b16c2deb8..9c95a5d835 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -12,6 +12,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable +import android.view.View import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle @@ -392,4 +393,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity val className = componentName.className return packageName == buildMeta.applicationId && className in allowList } + + override val rootView: View + get() = views.mainRoot } diff --git a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt index e3cf06bd87..42598f4356 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/ui/consent/AnalyticsOptInActivity.kt @@ -7,6 +7,7 @@ package im.vector.app.features.analytics.ui.consent +import android.view.View import com.airbnb.mvrx.viewModel import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment @@ -29,6 +30,9 @@ class AnalyticsOptInActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { orientationLocker.lockPhonesToPortrait(this) if (isFirstCreation()) { diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt index 487e116fb5..5382d54670 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewActivity.kt @@ -9,6 +9,7 @@ package im.vector.app.features.attachments.preview import android.content.Context import android.content.Intent +import android.view.View import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.VectorBaseActivity @@ -47,6 +48,9 @@ class AttachmentsPreviewActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { if (isFirstCreation()) { val fragmentArgs: AttachmentsPreviewArgs = intent?.extras?.getParcelableCompat(EXTRA_FRAGMENT_ARGS) ?: return diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index 999b76295d..a4f1dafda1 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -163,6 +163,7 @@ class AttachmentsPreviewFragment : private fun applyInsets() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + @Suppress("DEPRECATION") activity?.window?.setDecorFitsSystemWindows(false) } else { @Suppress("DEPRECATION") diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 47db3dccc7..03a3aaf8c9 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -128,7 +128,9 @@ class VectorCallActivity : window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + @Suppress("DEPRECATION") window.statusBarColor = Color.TRANSPARENT + @Suppress("DEPRECATION") window.navigationBarColor = Color.BLACK super.onCreate(savedInstanceState) addOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfoConsumer) @@ -185,6 +187,9 @@ class VectorCallActivity : override fun getMenuRes() = R.menu.vector_call + override val rootView: View + get() = views.constraintLayout + override fun onUserLeaveHint() { super.onUserLeaveHint() enterPictureInPictureIfRequired() diff --git a/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt index 30d6507380..69adc08c63 100644 --- a/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt +++ b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt @@ -43,6 +43,11 @@ internal class API21AudioDeviceDetector( return HashSet().apply { if (isBluetoothHeadsetOn()) { connectedBlueToothHeadset?.connectedDevices?.forEach { + // Call requires permission which may be rejected by user: code should explicitly + // check to see if permission is available (with checkPermission) or explicitly + // handle a potential SecurityException + // But it should not happen on API 21/22. + @Suppress("MissingPermission") add(CallAudioManager.Device.WirelessHeadset(it.name)) } } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index 4a0ccf28e3..a86b5e34c9 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -15,6 +15,7 @@ import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.os.Parcelable +import android.view.View import android.widget.FrameLayout import android.widget.Toast import androidx.core.app.PictureInPictureModeChangedInfo @@ -58,6 +59,9 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee override fun getBinding() = ActivityJitsiBinding.inflate(layoutInflater) + override val rootView: View + get() = views.jitsiLayout + private var jitsiMeetView: JitsiMeetView? = null private val jitsiViewModel: JitsiCallViewModel by viewModel() diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt index 7efe7b8df9..5bb1b5d0dd 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt @@ -12,7 +12,6 @@ import android.content.ClipboardManager import android.content.Context import android.content.res.ColorStateList import android.os.Bundle -import android.telephony.PhoneNumberFormattingTextWatcher import android.telephony.PhoneNumberUtils import android.text.Editable import android.text.InputType @@ -78,7 +77,8 @@ class DialPadFragment : Fragment(), TextWatcher { digits.inputType = InputType.TYPE_CLASS_PHONE digits.keyListener = DialerKeyListener.getInstance() digits.setTextColor(ThemeUtils.getColor(requireContext(), im.vector.lib.ui.styles.R.attr.vctr_content_primary)) - digits.addTextChangedListener(PhoneNumberFormattingTextWatcher(if (formatAsYouType) regionCode else "")) + @Suppress("DEPRECATION") + digits.addTextChangedListener(android.telephony.PhoneNumberFormattingTextWatcher(if (formatAsYouType) regionCode else "")) digits.addTextChangedListener(this) dialpadView.findViewById(R.id.zero).setOnClickListener { keyPressed(KeyEvent.KEYCODE_0, "0") } dialpadView.findViewById(R.id.one).setOnClickListener { keyPressed(KeyEvent.KEYCODE_1, "1") } diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt index 6a647bd501..1925ce854e 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt @@ -11,6 +11,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable +import android.view.View import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.google.android.material.tabs.TabLayoutMediator @@ -37,6 +38,9 @@ class CallTransferActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.vectorCoordinatorLayout + override val rootView: View + get() = views.vectorCoordinatorLayout + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) waitingView = views.waitingView.waitingView diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt index b1072576bc..8a29bdd9e8 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt @@ -122,6 +122,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment= Build.VERSION_CODES.R) { + @Suppress("DEPRECATION") dialog?.window?.setDecorFitsSystemWindows(false) } else { @Suppress("DEPRECATION") diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index d22ea90856..bea9d1303b 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -192,6 +192,9 @@ class HomeActivity : override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun getBinding() = ActivityHomeBinding.inflate(layoutInflater) override fun onCreate(savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt index 4a3d5d2583..0ead96292a 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt @@ -13,7 +13,9 @@ import android.view.View import android.view.ViewGroup import androidx.core.app.ActivityOptionsCompat import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible +import androidx.core.view.updatePadding import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.observeK @@ -109,6 +111,20 @@ class HomeDrawerFragment : } } + ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + v.updatePadding( + systemBars.left, + systemBars.top, + systemBars.right, + systemBars.bottom, + ) + WindowInsetsCompat.CONSUMED + } + // Debug menu views.homeDrawerHeaderDebugView.debouncedClicks { sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer) diff --git a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsFragment.kt index ccad10150a..6f32572420 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsFragment.kt @@ -11,6 +11,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint @@ -38,6 +41,19 @@ class BreadcrumbsFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + ViewCompat.setOnApplyWindowInsetsListener(views.breadcrumbsRecyclerView) { v, insets -> + val systemBars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() or + WindowInsetsCompat.Type.displayCutout() + ) + v.updatePadding( + systemBars.left, + systemBars.top, + systemBars.right, + systemBars.bottom, + ) + WindowInsetsCompat.CONSUMED + } setupRecyclerView() sharedActionViewModel = activityViewModelProvider.get(RoomDetailSharedActionViewModel::class.java) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt index e65e6c17f0..e5d785af63 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt @@ -14,7 +14,6 @@ import android.os.Bundle import android.view.View import android.widget.Toast import androidx.core.view.GravityCompat -import androidx.core.view.WindowCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -82,6 +81,9 @@ class RoomDetailActivity : override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + @Inject lateinit var playbackTracker: AudioMessagePlaybackTracker private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel() @@ -93,7 +95,7 @@ class RoomDetailActivity : super.onCreate(savedInstanceState) // For dealing with insets and status bar background color - WindowCompat.setDecorFitsSystemWindows(window, false) + @Suppress("DEPRECATION") window.statusBarColor = Color.TRANSPARENT supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 8e7b9ca951..01be29d84f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -32,11 +32,9 @@ import androidx.core.net.toUri import androidx.core.text.toSpannable import androidx.core.util.Pair import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat import androidx.core.view.forEach import androidx.core.view.isInvisible import androidx.core.view.isVisible -import androidx.core.view.updatePadding import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.lifecycleScope import androidx.lifecycle.withResumed @@ -409,13 +407,6 @@ class TimelineFragment : is RoomDetailViewEvents.RevokeFilePermission -> revokeFilePermission(it) } } - - ViewCompat.setOnApplyWindowInsetsListener(views.coordinatorLayout) { _, insets -> - val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.systemBars()) - views.appBarLayout.updatePadding(top = imeInsets.top) - views.voiceMessageRecorderContainer.updatePadding(bottom = imeInsets.bottom) - insets - } } private fun setupBackPressHandling() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 618b31796f..f92a062546 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -54,7 +54,6 @@ import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.location.live.StopLiveLocationShareUseCase import im.vector.app.features.location.live.tracking.LocationSharingServiceConnection import im.vector.app.features.notifications.NotificationDrawerManager -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.raw.wellknown.CryptoConfig import im.vector.app.features.raw.wellknown.getOutboundSessionKeySharingStrategyOrDefault import im.vector.app.features.raw.wellknown.withElementWellKnown @@ -110,7 +109,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachme import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -303,12 +301,12 @@ class TimelineViewModel @AssistedInject constructor( private fun observePowerLevel() { if (room == null) return - PowerLevelsFlowFactory(room).createFlow() - .onEach { - val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId) + room.flow().liveRoomPowerLevels() + .onEach { powerLevels -> + val canInvite = powerLevels.isUserAbleToInvite(session.myUserId) val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId) - val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE) - val isAllowedToSetupEncryption = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) + val isAllowedToStartWebRTCCall = powerLevels.isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE) + val isAllowedToSetupEncryption = powerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) setState { copy( canInvite = canInvite, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 928adb63f4..4e645f93ae 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -30,7 +30,6 @@ import im.vector.app.features.home.room.detail.ChatEffect import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import im.vector.app.features.home.room.detail.toMessageType -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.voice.VoiceFailure @@ -69,7 +68,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent @@ -180,10 +178,10 @@ class MessageComposerViewModel @AssistedInject constructor( private fun observePowerLevelAndEncryption(room: Room) { combine( - PowerLevelsFlowFactory(room).createFlow(), + room.flow().liveRoomPowerLevels(), room.flow().liveRoomSummary().unwrap() ) { pl, sum -> - val canSendMessage = PowerLevelsHelper(pl).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) + val canSendMessage = pl.isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) if (canSendMessage) { val isE2E = sum.isEncrypted if (isE2E) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt index 0d674174c8..f262f5b044 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.home.room.detail.search import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import androidx.appcompat.widget.SearchView import com.airbnb.mvrx.Mavericks import dagger.hilt.android.AndroidEntryPoint @@ -30,6 +31,9 @@ class SearchActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupToolbar(views.searchToolbar) 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 1028a0548f..2280a26842 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 @@ -23,7 +23,6 @@ import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormat import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.VectorHtmlCompressor -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.settings.VectorPreferences import im.vector.lib.strings.CommonStrings @@ -49,7 +48,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited @@ -116,12 +114,11 @@ class MessageActionsViewModel @AssistedInject constructor( if (room == null) { return } - PowerLevelsFlowFactory(room).createFlow() - .onEach { - val powerLevelsHelper = PowerLevelsHelper(it) - val canReact = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.REACTION) - val canRedact = powerLevelsHelper.isUserAbleToRedact(session.myUserId) - val canSendMessage = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) + room.flow().liveRoomPowerLevels() + .onEach { roomPowerLevels -> + val canReact = roomPowerLevels.isUserAllowedToSend(session.myUserId, false, EventType.REACTION) + val canRedact = roomPowerLevels.isUserAbleToRedact(session.myUserId) + val canSendMessage = roomPowerLevels.isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) val permissions = ActionPermissions(canSendMessage = canSendMessage, canRedact = canRedact, canReact = canReact) setState { copy(actionPermissions = permissions) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 45ba700d1f..012421bf8d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -24,16 +24,13 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement import im.vector.lib.strings.CommonPlurals import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.getStateEvent -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.getRoomPowerLevels import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject @@ -303,9 +300,7 @@ class MergedHeaderItemFactory @Inject constructor( collapsedEventIds.removeAll(mergedEventIds) } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } - val powerLevelsHelper = activeSessionHolder.getSafeActiveSession()?.getRoom(event.roomId) - ?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)?.content?.toModel() } - ?.let { PowerLevelsHelper(it) } + val roomPowerLevels = activeSessionHolder.getSafeActiveSession()?.getRoom(event.roomId)?.getRoomPowerLevels() val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: "" val attributes = MergedRoomCreationItem.Attributes( isCollapsed = isCollapsed, @@ -320,10 +315,10 @@ class MergedHeaderItemFactory @Inject constructor( callback = callback, currentUserId = currentUserId, roomSummary = partialState.roomSummary, - canInvite = powerLevelsHelper?.isUserAbleToInvite(currentUserId) ?: false, - canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false, - canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false, - canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false + canInvite = roomPowerLevels?.isUserAbleToInvite(currentUserId) ?: false, + canChangeAvatar = roomPowerLevels?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false, + canChangeTopic = roomPowerLevels?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false, + canChangeName = roomPowerLevels?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false ) MergedRoomCreationItem_() .id(mergeId) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index bd1c903f1f..aea064736a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -41,7 +41,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent import org.matrix.android.sdk.api.session.room.model.RoomTopicContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.widgets.model.WidgetContent import timber.log.Timber @@ -122,8 +122,8 @@ class NoticeEventFormatter @Inject constructor( userIds.addAll(previousPowerLevelsContent.users.orEmpty().keys) val diffs = ArrayList() userIds.forEach { userId -> - val from = PowerLevelsHelper(previousPowerLevelsContent).getUserRole(userId) - val to = PowerLevelsHelper(powerLevelsContent).getUserRole(userId) + val from = RoomPowerLevels(previousPowerLevelsContent, null).getSuggestedRole(userId) + val to = RoomPowerLevels(powerLevelsContent, null).getSuggestedRole(userId) if (from != to) { val fromStr = roleFormatter.format(from) val toStr = roleFormatter.format(to) diff --git a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt index b2402ee439..7e10e69797 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.home.room.filtered import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import androidx.appcompat.widget.SearchView import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.replaceFragment @@ -32,6 +33,9 @@ class FilteredRoomsActivity : VectorBaseActivity() override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) analyticsScreenName = MobileScreen.ScreenName.RoomFilter diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 1e04880382..51b413058e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -25,7 +25,6 @@ import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.epoxy.LayoutManagerStateRestorer @@ -45,6 +44,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import im.vector.app.features.home.room.list.widget.NotifsFabMenuView import im.vector.app.features.matrixto.OriginOfMatrixTo import im.vector.app.features.notifications.NotificationDrawerManager +import im.vector.app.features.room.LeaveRoomPrompt import im.vector.lib.strings.CommonStrings import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn @@ -422,7 +422,7 @@ class RoomListFragment : } } - private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) { + private suspend fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) { when (quickAction) { is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> { roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY)) @@ -451,26 +451,11 @@ class RoomListFragment : } } - private fun promptLeaveRoom(roomId: String) { - val isPublicRoom = roomListViewModel.isPublicRoom(roomId) - val message = buildString { - append(getString(CommonStrings.room_participants_leave_prompt_msg)) - if (!isPublicRoom) { - append("\n\n") - append(getString(CommonStrings.room_participants_leave_private_warning)) - } + private suspend fun promptLeaveRoom(roomId: String) { + val warning = roomListViewModel.getLeaveRoomWarning(roomId) + LeaveRoomPrompt.show(requireContext(), warning) { + roomListViewModel.handle(RoomListAction.LeaveRoom(roomId)) } - MaterialAlertDialogBuilder( - requireContext(), - if (isPublicRoom) 0 else im.vector.lib.ui.styles.R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive - ) - .setTitle(CommonStrings.room_participants_leave_prompt_title) - .setMessage(message) - .setPositiveButton(CommonStrings.action_leave) { _, _ -> - roomListViewModel.handle(RoomListAction.LeaveRoom(roomId)) - } - .setNegativeButton(CommonStrings.action_cancel, null) - .show() } override fun invalidate() = withState(roomListViewModel) { state -> diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 907c4cca72..a4dae15519 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -26,6 +26,8 @@ import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.displayname.getBestName import im.vector.app.features.invite.AutoAcceptInvites +import im.vector.app.features.room.LeaveRoomPrompt +import im.vector.app.features.room.getLeaveRoomWarning import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged @@ -41,7 +43,6 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.api.session.room.state.isPublic import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.flow.flow import timber.log.Timber @@ -150,8 +151,8 @@ class RoomListViewModel @AssistedInject constructor( } } - fun isPublicRoom(roomId: String): Boolean { - return session.getRoom(roomId)?.stateService()?.isPublic().orFalse() + suspend fun getLeaveRoomWarning(roomId: String): LeaveRoomPrompt.Warning { + return session.getLeaveRoomWarning(roomId) } // PRIVATE METHODS ***************************************************************************** diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt index 02d702685a..a6421f19a0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt @@ -18,7 +18,6 @@ import androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode import androidx.recyclerview.widget.LinearLayoutManager import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.mvrx.fragmentViewModel -import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.extensions.cleanup @@ -36,7 +35,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import im.vector.app.features.home.room.list.home.header.HomeRoomFilter import im.vector.app.features.home.room.list.home.header.HomeRoomsHeadersController import im.vector.app.features.home.room.list.home.invites.InvitesActivity -import im.vector.lib.strings.CommonStrings +import im.vector.app.features.room.LeaveRoomPrompt import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -103,7 +102,7 @@ class HomeRoomListFragment : } } - private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) { + private suspend fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) { when (quickAction) { is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> { roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY)) @@ -185,26 +184,11 @@ class HomeRoomListFragment : concatAdapter.addAdapter(roomsAdapter) } - private fun promptLeaveRoom(roomId: String) { - val isPublicRoom = roomListViewModel.isPublicRoom(roomId) - val message = buildString { - append(getString(CommonStrings.room_participants_leave_prompt_msg)) - if (!isPublicRoom) { - append("\n\n") - append(getString(CommonStrings.room_participants_leave_private_warning)) - } + private suspend fun promptLeaveRoom(roomId: String) { + val warning = roomListViewModel.getLeaveRoomWarning(roomId) + LeaveRoomPrompt.show(requireContext(), warning) { + roomListViewModel.handle(HomeRoomListAction.LeaveRoom(roomId)) } - MaterialAlertDialogBuilder( - requireContext(), - if (isPublicRoom) 0 else im.vector.lib.ui.styles.R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive - ) - .setTitle(CommonStrings.room_participants_leave_prompt_title) - .setMessage(message) - .setPositiveButton(CommonStrings.action_leave) { _, _ -> - roomListViewModel.handle(HomeRoomListAction.LeaveRoom(roomId)) - } - .setNegativeButton(CommonStrings.action_cancel, null) - .show() } private fun onInvitesCounterClicked() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt index d232dab3fb..b84de29630 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt @@ -26,6 +26,8 @@ import im.vector.app.features.analytics.extensions.toTrackingValue import im.vector.app.features.analytics.plan.UserProperties import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.list.home.header.HomeRoomFilter +import im.vector.app.features.room.LeaveRoomPrompt +import im.vector.app.features.room.getLeaveRoomWarning import im.vector.lib.strings.CommonStrings import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -53,7 +55,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.api.session.room.state.isPublic import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toOption @@ -331,8 +332,8 @@ class HomeRoomListViewModel @AssistedInject constructor( filteredPagedRoomSummariesLive.queryParams = getFilteredQueryParams(newFilter, filteredPagedRoomSummariesLive.queryParams) } - fun isPublicRoom(roomId: String): Boolean { - return session.getRoom(roomId)?.stateService()?.isPublic().orFalse() + suspend fun getLeaveRoomWarning(roomId: String): LeaveRoomPrompt.Warning { + return session.getLeaveRoomWarning(roomId) } private fun handleSelectRoom(action: HomeRoomListAction.SelectRoom) = withState { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesActivity.kt index f2a19545fe..8976e580c5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesActivity.kt @@ -7,6 +7,7 @@ package im.vector.app.features.home.room.list.home.invites +import android.view.View import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.VectorBaseActivity @@ -22,4 +23,8 @@ class InvitesActivity : VectorBaseActivity() { addFragment(views.simpleFragmentContainer, InvitesFragment::class.java) } } + + override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesActivity.kt index 0a3fa4393a..e7c104ff05 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/release/ReleaseNotesActivity.kt @@ -7,6 +7,7 @@ package im.vector.app.features.home.room.list.home.release +import android.view.View import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment @@ -26,6 +27,9 @@ class ReleaseNotesActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { orientationLocker.lockPhonesToPortrait(this) if (isFirstCreation()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt index e0b773c2b6..9174f60e5b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/ThreadsActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.home.room.threads import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import androidx.fragment.app.FragmentTransaction import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragmentToBackstack @@ -42,6 +43,9 @@ class ThreadsActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initFragment() diff --git a/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt b/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt index fcb0dc22a8..968c422f43 100644 --- a/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt +++ b/vector/src/main/java/im/vector/app/features/lifecycle/VectorActivityLifecycleCallbacks.kt @@ -32,7 +32,7 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager /** * The activities information collected from the app manifest. */ - private var activitiesInfo: Array = emptyArray() + private var activitiesInfo: List? = null private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -51,24 +51,32 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager override fun onActivityStopped(activity: Activity) {} override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - if (activitiesInfo.isEmpty()) { + if (activitiesInfo == null) { val context = activity.applicationContext val packageManager: PackageManager = context.packageManager // Get all activities from element android - activitiesInfo = packageManager.getPackageInfoCompat(context.packageName, PackageManager.GET_ACTIVITIES).activities - + val activities = packageManager + .getPackageInfoCompat(context.packageName, PackageManager.GET_ACTIVITIES) + .activities + .orEmpty() + .toList() // Get all activities from PermissionController module // See https://source.android.com/docs/core/architecture/modular-system/permissioncontroller#package-format - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) { - activitiesInfo += tryOrNull { + val otherActivities = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) { + (tryOrNull { packageManager.getPackageInfoCompat("com.google.android.permissioncontroller", PackageManager.GET_ACTIVITIES).activities } ?: tryOrNull { packageManager.getModuleInfo("com.google.android.permission", 1).packageName?.let { packageManager.getPackageInfoCompat(it, PackageManager.GET_ACTIVITIES or PackageManager.MATCH_APEX).activities } - }.orEmpty() + }) + .orEmpty() + .toList() + } else { + emptyList() } + activitiesInfo = activities + otherActivities } // restart the app if the task contains an unknown activity @@ -144,5 +152,5 @@ class VectorActivityLifecycleCallbacks constructor(private val popupAlertManager * @param activity the activity of the task * @return true if the activity is potentially malicious */ - private fun isPotentialMaliciousActivity(activity: ComponentName): Boolean = activitiesInfo.none { it.name == activity.className } + private fun isPotentialMaliciousActivity(activity: ComponentName): Boolean = activitiesInfo.orEmpty().none { it.name == activity.className } } diff --git a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt index 5c3ba3fbc3..f8c9057571 100644 --- a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.link import android.content.Intent import android.net.Uri import android.os.Bundle +import android.view.View import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -41,6 +42,9 @@ class LinkHandlerActivity : VectorBaseActivity() { override fun getBinding() = ActivityProgressBinding.inflate(layoutInflater) + override val rootView: View + get() = views.mainRoot + override fun initUiAndData() { handleIntent() } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt index d9d3ee0731..70ef34d03b 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.location import android.content.Context import android.content.Intent import android.os.Parcelable +import android.view.View import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.VectorBaseActivity @@ -31,6 +32,9 @@ class LocationSharingActivity : VectorBaseActivity(initialState), LocationTracker.Callback { private val room = session.getRoom(initialState.roomId)!! @@ -70,13 +72,12 @@ class LocationSharingViewModel @AssistedInject constructor( } private fun observePowerLevelsForLiveLocationSharing() { - PowerLevelsFlowFactory(room).createFlow() + room.flow().liveRoomPowerLevels() .distinctUntilChanged() - .setOnEach { - val powerLevelsHelper = PowerLevelsHelper(it) + .setOnEach { roomPowerLevels -> val canShareLiveLocation = EventType.STATE_ROOM_BEACON_INFO.values .all { beaconInfoType -> - powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, beaconInfoType) + roomPowerLevels.isUserAllowedToSend(session.myUserId, true, beaconInfoType) } copy(canShareLiveLocation = canShareLiveLocation) @@ -88,7 +89,15 @@ class LocationSharingViewModel @AssistedInject constructor( locationTracker.locations .onEach(::onLocationUpdate) .launchIn(viewModelScope) - locationTracker.start() + if (permissionChecker.checkPermission( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + ) + ) { + locationTracker.start() + } else { + Timber.w("Not allowed to use location api.") + } } private fun setUserItem() { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index 70e909de27..1baebec617 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -17,6 +17,7 @@ import androidx.core.content.getSystemService import androidx.core.location.LocationListenerCompat import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.resources.BuildMeta +import im.vector.app.core.utils.PermissionChecker import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -37,6 +38,7 @@ class LocationTracker @Inject constructor( context: Context, private val activeSessionHolder: ActiveSessionHolder, private val buildMeta: BuildMeta, + private val permissionChecker: PermissionChecker, ) : LocationListenerCompat { private val locationManager = context.getSystemService() @@ -173,7 +175,15 @@ class LocationTracker @Inject constructor( fun removeCallback(callback: Callback) { callbacks.remove(callback) if (callbacks.size == 0) { - stop() + if (permissionChecker.checkPermission( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + ) + ) { + stop() + } else { + Timber.w("Not allowed to use location api.") + } } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewActivity.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewActivity.kt index 21e8f2ba02..2564d5a395 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LiveLocationMapViewActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.location.live.map import android.content.Context import android.content.Intent import android.os.Parcelable +import android.view.View import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.VectorBaseActivity @@ -29,6 +30,9 @@ class LiveLocationMapViewActivity : VectorBaseActivity(initialState), LocationSharingServiceConnection.Callback, @@ -123,7 +127,15 @@ class LiveLocationMapViewModel @AssistedInject constructor( copy(isLoadingUserLocation = true) } viewModelScope.launch(session.coroutineDispatchers.main) { - locationTracker.start() + if (permissionChecker.checkPermission( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + ) + ) { + locationTracker.start() + } else { + Timber.w("Not allowed to use location api.") + } locationTracker.requestLastKnownLocation() } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt b/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt index 315a555703..76205f98cf 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt @@ -7,14 +7,18 @@ package im.vector.app.features.location.live.tracking +import android.Manifest import android.content.Intent +import android.content.pm.PackageManager import android.os.IBinder import android.os.Parcelable +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.startForegroundCompat import im.vector.app.core.services.VectorAndroidService +import im.vector.app.core.utils.PermissionChecker import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationTracker import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase @@ -52,6 +56,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase @Inject lateinit var checkIfEventIsRedactedUseCase: CheckIfEventIsRedactedUseCase + @Inject lateinit var permissionChecker: PermissionChecker private var binder: LocationSharingAndroidServiceBinder? = null @@ -74,7 +79,15 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca private fun initLocationTracking() { // Start tracking location locationTracker.addCallback(this) - locationTracker.start() + if (permissionChecker.checkPermission( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + ) + ) { + locationTracker.start() + } else { + Timber.w("Not allowed to use location api.") + } launchWithActiveSession { session -> val job = locationTracker.locations @@ -95,7 +108,11 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca // Show a sticky notification val notification = liveLocationNotificationBuilder.buildLiveLocationSharingNotification(roomArgs.roomId) if (foregroundModeStarted) { - NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + Timber.w("Not allowed to notify.") + } else { + NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) + } } else { startForegroundCompat(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) foregroundModeStarted = true @@ -146,10 +163,14 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca } private fun updateNotification() { - if (liveInfoSet.isNotEmpty()) { - val roomId = liveInfoSet.last().roomArgs.roomId - val notification = liveLocationNotificationBuilder.buildLiveLocationSharingNotification(roomId) - NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + Timber.w("Not allowed to notify.") + } else { + if (liveInfoSet.isNotEmpty()) { + val roomId = liveInfoSet.last().roomArgs.roomId + val notification = liveLocationNotificationBuilder.buildLiveLocationSharingNotification(roomId) + NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) + } } } diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt index aacff1f745..ae091adaae 100644 --- a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt @@ -7,6 +7,7 @@ package im.vector.app.features.location.preview +import android.Manifest import com.airbnb.mvrx.MavericksViewModelFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -14,6 +15,7 @@ import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.utils.PermissionChecker import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationTracker @@ -23,12 +25,14 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem +import timber.log.Timber class LocationPreviewViewModel @AssistedInject constructor( @Assisted private val initialState: LocationPreviewViewState, private val session: Session, private val locationPinProvider: LocationPinProvider, private val locationTracker: LocationTracker, + private val permissionChecker: PermissionChecker, ) : VectorViewModel(initialState), LocationTracker.Callback { @AssistedFactory @@ -89,7 +93,15 @@ class LocationPreviewViewModel @AssistedInject constructor( copy(isLoadingUserLocation = true) } viewModelScope.launch(session.coroutineDispatchers.main) { - locationTracker.start() + if (permissionChecker.checkPermission( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + ) + ) { + locationTracker.start() + } else { + Timber.w("Not allowed to use location api.") + } locationTracker.requestLastKnownLocation() } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index cbf13acc98..2419ba7bfc 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -76,6 +76,9 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { analyticsScreenName = MobileScreen.ScreenName.Login diff --git a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt index 05c331d1d0..57d631b5be 100644 --- a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.media import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import androidx.core.net.toUri import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.di.ActiveSessionHolder @@ -26,6 +27,9 @@ class BigImageViewerActivity : VectorBaseActivity override fun getBinding() = ActivityBigImageViewerBinding.inflate(layoutInflater) + override val rootView: View + get() = views.mainRoot + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt index 058372feff..e6e2209e42 100644 --- a/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/media/VectorAttachmentViewerActivity.kt @@ -138,7 +138,9 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), AttachmentInt } } + @Suppress("DEPRECATION") window.statusBarColor = ContextCompat.getColor(this, im.vector.lib.ui.styles.R.color.black_alpha) + @Suppress("DEPRECATION") window.navigationBarColor = ContextCompat.getColor(this, im.vector.lib.ui.styles.R.color.black_alpha) observeViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDisplayer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDisplayer.kt index 5106eb1dfc..29eeb04f9d 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDisplayer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDisplayer.kt @@ -7,18 +7,27 @@ package im.vector.app.features.notifications +import android.Manifest import android.app.Notification import android.content.Context +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationManagerCompat import timber.log.Timber import javax.inject.Inject -class NotificationDisplayer @Inject constructor(context: Context) { +class NotificationDisplayer @Inject constructor( + private val context: Context, +) { private val notificationManager = NotificationManagerCompat.from(context) fun showNotificationMessage(tag: String?, id: Int, notification: Notification) { - notificationManager.notify(tag, id, notification) + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + Timber.w("Not allowed to notify.") + } else { + notificationManager.notify(tag, id, notification) + } } fun cancelNotificationMessage(tag: String?, id: Int) { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 985d35961f..aa6010c31e 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -9,6 +9,7 @@ package im.vector.app.features.notifications +import android.Manifest import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel @@ -16,6 +17,7 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Canvas import android.net.Uri @@ -27,6 +29,7 @@ import androidx.annotation.AttrRes import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput @@ -153,55 +156,59 @@ class NotificationUtils @Inject constructor( * Default notification importance: shows everywhere, makes noise, but does not visually * intrude. */ - notificationManager.createNotificationChannel(NotificationChannel( - NOISY_NOTIFICATION_CHANNEL_ID, - stringProvider.getString(CommonStrings.notification_noisy_notifications).ifEmpty { "Noisy notifications" }, - NotificationManager.IMPORTANCE_DEFAULT - ) - .apply { - description = stringProvider.getString(CommonStrings.notification_noisy_notifications) - enableVibration(true) - enableLights(true) - lightColor = accentColor - }) + notificationManager.createNotificationChannel( + NotificationChannel( + NOISY_NOTIFICATION_CHANNEL_ID, + stringProvider.getString(CommonStrings.notification_noisy_notifications).ifEmpty { "Noisy notifications" }, + NotificationManager.IMPORTANCE_DEFAULT + ) + .apply { + description = stringProvider.getString(CommonStrings.notification_noisy_notifications) + enableVibration(true) + enableLights(true) + lightColor = accentColor + }) /** * Low notification importance: shows everywhere, but is not intrusive. */ - notificationManager.createNotificationChannel(NotificationChannel( - SILENT_NOTIFICATION_CHANNEL_ID, - stringProvider.getString(CommonStrings.notification_silent_notifications).ifEmpty { "Silent notifications" }, - NotificationManager.IMPORTANCE_LOW - ) - .apply { - description = stringProvider.getString(CommonStrings.notification_silent_notifications) - setSound(null, null) - enableLights(true) - lightColor = accentColor - }) + notificationManager.createNotificationChannel( + NotificationChannel( + SILENT_NOTIFICATION_CHANNEL_ID, + stringProvider.getString(CommonStrings.notification_silent_notifications).ifEmpty { "Silent notifications" }, + NotificationManager.IMPORTANCE_LOW + ) + .apply { + description = stringProvider.getString(CommonStrings.notification_silent_notifications) + setSound(null, null) + enableLights(true) + lightColor = accentColor + }) - notificationManager.createNotificationChannel(NotificationChannel( - LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, - stringProvider.getString(CommonStrings.notification_listening_for_events).ifEmpty { "Listening for events" }, - NotificationManager.IMPORTANCE_MIN - ) - .apply { - description = stringProvider.getString(CommonStrings.notification_listening_for_events) - setSound(null, null) - setShowBadge(false) - }) + notificationManager.createNotificationChannel( + NotificationChannel( + LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID, + stringProvider.getString(CommonStrings.notification_listening_for_events).ifEmpty { "Listening for events" }, + NotificationManager.IMPORTANCE_MIN + ) + .apply { + description = stringProvider.getString(CommonStrings.notification_listening_for_events) + setSound(null, null) + setShowBadge(false) + }) - notificationManager.createNotificationChannel(NotificationChannel( - CALL_NOTIFICATION_CHANNEL_ID, - stringProvider.getString(CommonStrings.call).ifEmpty { "Call" }, - NotificationManager.IMPORTANCE_HIGH - ) - .apply { - description = stringProvider.getString(CommonStrings.call) - setSound(null, null) - enableLights(true) - lightColor = accentColor - }) + notificationManager.createNotificationChannel( + NotificationChannel( + CALL_NOTIFICATION_CHANNEL_ID, + stringProvider.getString(CommonStrings.call).ifEmpty { "Call" }, + NotificationManager.IMPORTANCE_HIGH + ) + .apply { + description = stringProvider.getString(CommonStrings.call) + setSound(null, null) + enableLights(true) + lightColor = accentColor + }) } fun getChannel(channelId: String): NotificationChannel? { @@ -997,7 +1004,11 @@ class NotificationUtils @Inject constructor( } fun showNotificationMessage(tag: String?, id: Int, notification: Notification) { - notificationManager.notify(tag, id, notification) + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + Timber.w("Not allowed to notify.") + } else { + notificationManager.notify(tag, id, notification) + } } fun cancelNotificationMessage(tag: String?, id: Int) { @@ -1025,30 +1036,34 @@ class NotificationUtils @Inject constructor( @SuppressLint("LaunchActivityFromNotification") fun displayDiagnosticNotification() { - val testActionIntent = Intent(context, TestNotificationReceiver::class.java) - testActionIntent.action = actionIds.diagnostic - val testPendingIntent = PendingIntent.getBroadcast( - context, - 0, - testActionIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + Timber.w("Not allowed to notify.") + } else { + val testActionIntent = Intent(context, TestNotificationReceiver::class.java) + testActionIntent.action = actionIds.diagnostic + val testPendingIntent = PendingIntent.getBroadcast( + context, + 0, + testActionIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) - notificationManager.notify( - "DIAGNOSTIC", - 888, - NotificationCompat.Builder(context, NOISY_NOTIFICATION_CHANNEL_ID) - .setContentTitle(buildMeta.applicationName) - .setContentText(stringProvider.getString(CommonStrings.settings_troubleshoot_test_push_notification_content)) - .setSmallIcon(R.drawable.ic_notification) - .setLargeIcon(getBitmap(context, im.vector.lib.ui.styles.R.drawable.element_logo_green)) - .setColor(ContextCompat.getColor(context, im.vector.lib.ui.styles.R.color.notification_accent_color)) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setCategory(NotificationCompat.CATEGORY_STATUS) - .setAutoCancel(true) - .setContentIntent(testPendingIntent) - .build() - ) + notificationManager.notify( + "DIAGNOSTIC", + 888, + NotificationCompat.Builder(context, NOISY_NOTIFICATION_CHANNEL_ID) + .setContentTitle(buildMeta.applicationName) + .setContentText(stringProvider.getString(CommonStrings.settings_troubleshoot_test_push_notification_content)) + .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(getBitmap(context, im.vector.lib.ui.styles.R.drawable.element_logo_green)) + .setColor(ContextCompat.getColor(context, im.vector.lib.ui.styles.R.color.notification_accent_color)) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setAutoCancel(true) + .setContentIntent(testPendingIntent) + .build() + ) + } } private fun getBitmap(context: Context, @DrawableRes drawableRes: Int): Bitmap? { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt index f62989fec6..dd8d287bad 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.onboarding import android.content.Context import android.content.Intent import android.net.Uri +import android.view.View import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.lazyViewModel import im.vector.app.core.extensions.validateBackPressed @@ -33,6 +34,9 @@ class OnboardingActivity : VectorBaseActivity(), UnlockedA override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) onboardingVariant.onNewIntent(intent) diff --git a/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt b/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt index a002bc0329..b5c70fddd9 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt @@ -9,6 +9,7 @@ package im.vector.app.features.pin import android.content.Context import android.content.Intent +import android.view.View import com.airbnb.mvrx.Mavericks import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment @@ -31,6 +32,9 @@ class PinActivity : VectorBaseActivity(), UnlockedActivit override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { if (isFirstCreation()) { val fragmentArgs: PinArgs = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG) ?: return diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/views/LockScreenCodeView.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/views/LockScreenCodeView.kt index 5187b8ba73..6efe59b09a 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/views/LockScreenCodeView.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/views/LockScreenCodeView.kt @@ -92,7 +92,7 @@ class LockScreenCodeView @JvmOverloads constructor( */ fun deleteLast(): Int { if (code.size == 0) return code.size - code.removeLast() + code.removeAt(code.lastIndex) getCodeView(code.size)?.toggle() return code.size } diff --git a/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt b/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt deleted file mode 100644 index f065334ae7..0000000000 --- a/vector/src/main/java/im/vector/app/features/powerlevel/PowerLevelsFlowFactory.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2020-2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package im.vector.app.features.powerlevel - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.Room -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent -import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.flow.mapOptional -import org.matrix.android.sdk.flow.unwrap - -class PowerLevelsFlowFactory(private val room: Room) { - - fun createFlow(): Flow { - return room.flow() - .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - .mapOptional { it.content.toModel() } - .flowOn(Dispatchers.Default) - .unwrap() - } -} diff --git a/vector/src/main/java/im/vector/app/features/powerlevel/Role.kt b/vector/src/main/java/im/vector/app/features/powerlevel/Role.kt new file mode 100644 index 0000000000..5330c3c4be --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/powerlevel/Role.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package im.vector.app.features.powerlevel + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.flow.flow + +fun Role.isOwner() = this == Role.Creator || this == Role.SuperAdmin + +fun Room.membersByRoleFlow(): Flow>> { + val roomMembersFlow = flow().liveRoomMembers(roomMemberQueryParams()) + val roomPowerLevelsFlow = flow().liveRoomPowerLevels() + return combine(roomMembersFlow, roomPowerLevelsFlow) { roomMembers, roomPowerLevels -> + roomMembers.groupBy { roomPowerLevels.getSuggestedRole(it.userId) } + }.distinctUntilChanged() +} + +fun Room.isLastAdminFlow(userId: String): Flow { + return membersByRoleFlow().map { membersByRole -> + val creatorMembers = membersByRole[Role.Creator].orEmpty() + val superAdminMembers = membersByRole[Role.SuperAdmin].orEmpty() + val adminMembers = membersByRole[Role.Admin].orEmpty() + val joinedAdmins = (adminMembers + creatorMembers + superAdminMembers).filter { it.membership == Membership.JOIN } + if (joinedAdmins.size == 1) { + joinedAdmins.first().userId == userId + } else { + false + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt index d2f8016bda..5325a3f043 100644 --- a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.qrcode import android.app.Activity import android.content.Intent import android.os.Bundle +import android.view.View import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import com.airbnb.mvrx.viewModel @@ -26,6 +27,9 @@ class QrCodeScannerActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + private val qrViewModel: QrCodeScannerViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt index 71b5895573..b4024fa124 100755 --- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt +++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt @@ -11,6 +11,7 @@ import android.content.Context import android.content.Intent import android.view.Menu import android.view.MenuItem +import android.view.View import android.widget.Toast import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged @@ -37,6 +38,9 @@ class BugReportActivity : private val viewModel: BugReportViewModel by viewModel() + override val rootView: View + get() = views.mainRoot + private var reportType: ReportType = ReportType.BUG_REPORT override fun initUiAndData() { diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt index 7cd7b79b11..774b41245c 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt @@ -13,6 +13,7 @@ import android.graphics.Typeface import android.util.TypedValue import android.view.Menu import android.view.MenuItem +import android.view.View import android.widget.SearchView import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope @@ -55,6 +56,9 @@ class EmojiReactionPickerActivity : override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun getTitleRes() = CommonStrings.title_activity_emoji_reaction_picker @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider diff --git a/vector/src/main/java/im/vector/app/features/room/LeaveRoomPrompt.kt b/vector/src/main/java/im/vector/app/features/room/LeaveRoomPrompt.kt new file mode 100644 index 0000000000..ac06dd471c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/room/LeaveRoomPrompt.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package im.vector.app.features.room + +import android.content.Context +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.features.powerlevel.isLastAdminFlow +import im.vector.app.features.room.LeaveRoomPrompt.Warning +import im.vector.lib.strings.CommonStrings +import im.vector.lib.ui.styles.R +import kotlinx.coroutines.flow.first +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.state.isPublic + +object LeaveRoomPrompt { + + enum class Warning { + LAST_ADMIN, + PRIVATE_ROOM, + NONE + } + + fun show( + context: Context, + warning: Warning, + onLeaveClick: () -> Unit + ) { + val hasWarning = warning != Warning.NONE + val message = buildString { + append(context.getString(CommonStrings.room_participants_leave_prompt_msg)) + if (hasWarning) append("\n\n") + when (warning) { + Warning.LAST_ADMIN -> append(context.getString(CommonStrings.room_participants_leave_last_admin)) + Warning.PRIVATE_ROOM -> append(context.getString(CommonStrings.room_participants_leave_private_warning)) + Warning.NONE -> Unit + } + } + MaterialAlertDialogBuilder( + context, + if (hasWarning) R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive else 0 + ) + .setTitle(CommonStrings.room_participants_leave_prompt_title) + .setMessage(message) + .setPositiveButton(CommonStrings.action_leave) { _, _ -> + onLeaveClick() + } + .setNegativeButton(CommonStrings.action_cancel, null) + .show() + } +} + +suspend fun Session.getLeaveRoomWarning(roomId: String): Warning { + val room = getRoom(roomId) ?: return Warning.NONE + val isLastAdmin = room.isLastAdminFlow(myUserId).first() + return when { + isLastAdmin -> Warning.LAST_ADMIN + !room.stateService().isPublic() -> Warning.PRIVATE_ROOM + else -> Warning.NONE + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt index 3940404af9..8a1c75ad61 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.roomdirectory import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.withState @@ -41,6 +42,9 @@ class RoomDirectoryActivity : VectorBaseActivity(), Matri override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) analyticsScreenName = MobileScreen.ScreenName.RoomDirectory diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt index c3ba0e7041..90be84d848 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt @@ -11,6 +11,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks import dagger.hilt.android.AndroidEntryPoint @@ -36,6 +37,9 @@ class CreateRoomActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { if (isFirstCreation()) { val fragmentArgs: CreateRoomArgs = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG) ?: return diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt index 77bac7dfa1..b6859f36df 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.roomdirectory.roompreview import android.content.Context import android.content.Intent import android.os.Parcelable +import android.view.View import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.VectorBaseActivity @@ -73,6 +74,9 @@ class RoomPreviewActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.simpleFragmentContainer + override fun initUiAndData() { if (isFirstCreation()) { val args = intent.getParcelableExtraCompat(ARG) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt index 8e764470c7..ade308339e 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileAction.kt @@ -8,6 +8,7 @@ package im.vector.app.features.roommemberprofile import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel sealed class RoomMemberProfileAction : VectorViewModelAction { object RetryFetchingInfo : RoomMemberProfileAction() @@ -18,7 +19,7 @@ sealed class RoomMemberProfileAction : VectorViewModelAction { object InviteUser : RoomMemberProfileAction() object VerifyUser : RoomMemberProfileAction() object ShareRoomMemberProfile : RoomMemberProfileAction() - data class SetPowerLevel(val previousValue: Int, val newValue: Int, val askForValidation: Boolean) : RoomMemberProfileAction() + data class SetPowerLevel(val previousValue: UserPowerLevel, val newValue: UserPowerLevel.Value, val askForValidation: Boolean) : RoomMemberProfileAction() data class SetUserColorOverride(val newColorSpec: String) : RoomMemberProfileAction() data class OpenOrCreateDm(val userId: String) : RoomMemberProfileAction() } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt index cf593c039e..43ee03fd58 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileActivity.kt @@ -9,6 +9,7 @@ package im.vector.app.features.roommemberprofile import android.content.Context import android.content.Intent +import android.view.View import android.widget.Toast import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel @@ -37,6 +38,11 @@ class RoomMemberProfileActivity : VectorBaseActivity() { return ActivitySimpleBinding.inflate(layoutInflater) } + override fun getCoordinatorLayout() = views.coordinatorLayout + + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { if (isFirstCreation()) { val fragmentArgs: RoomMemberProfileArgs = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG) ?: return diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index 95ee18682e..bef3c5bda5 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -17,8 +17,7 @@ import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.strings.CommonStrings import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper -import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel import javax.inject.Inject class RoomMemberProfileController @Inject constructor( @@ -38,7 +37,7 @@ class RoomMemberProfileController @Inject constructor( fun onOverrideColorClicked() fun onJumpToReadReceiptClicked() fun onMentionClicked() - fun onEditPowerLevel(currentRole: Role) + fun onEditPowerLevel(userPowerLevel: UserPowerLevel.Value) fun onKickClicked(isSpace: Boolean) fun onBanClicked(isSpace: Boolean, isUserBanned: Boolean) fun onCancelInviteClicked() @@ -243,14 +242,14 @@ class RoomMemberProfileController @Inject constructor( } private fun buildAdminSection(state: RoomMemberProfileViewState) { - val powerLevelsContent = state.powerLevelsContent ?: return val powerLevelsStr = state.userPowerLevelString() ?: return - val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) - val userPowerLevel = powerLevelsHelper.getUserRole(state.userId) - val myPowerLevel = powerLevelsHelper.getUserRole(session.myUserId) + val roomPowerLevels = state.roomPowerLevels ?: return + val userPowerLevel = roomPowerLevels.getUserPowerLevel(state.userId) + val myPowerLevel = roomPowerLevels.getUserPowerLevel(session.myUserId) if ((!state.isMine && myPowerLevel <= userPowerLevel)) { return } + if (userPowerLevel !is UserPowerLevel.Value) return val membership = state.asyncMembership() ?: return val canKick = !state.isMine && state.actionPermissions.canKick val canBan = !state.isMine && state.actionPermissions.canBan diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt index 29cb42a686..7f0ec9b57a 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -51,7 +51,7 @@ import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs import im.vector.lib.strings.CommonStrings import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel -import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject @@ -377,9 +377,9 @@ class RoomMemberProfileFragment : .show() } - override fun onEditPowerLevel(currentRole: Role) { - EditPowerLevelDialogs.showChoice(requireActivity(), CommonStrings.power_level_edit_title, currentRole) { newPowerLevel -> - viewModel.handle(RoomMemberProfileAction.SetPowerLevel(currentRole.value, newPowerLevel, true)) + override fun onEditPowerLevel(userPowerLevel: UserPowerLevel.Value) { + EditPowerLevelDialogs.showChoice(requireActivity(), CommonStrings.power_level_edit_title, userPowerLevel) { newPowerLevel -> + viewModel.handle(RoomMemberProfileAction.SetPowerLevel(userPowerLevel, newPowerLevel, true)) } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt index d806f521ad..11bb19c51b 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewEvents.kt @@ -8,6 +8,7 @@ package im.vector.app.features.roommemberprofile import im.vector.app.core.platform.VectorViewEvents +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel /** * Transient events for RoomMemberProfile. @@ -22,8 +23,8 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents { object OnInviteActionSuccess : RoomMemberProfileViewEvents() object OnKickActionSuccess : RoomMemberProfileViewEvents() object OnBanActionSuccess : RoomMemberProfileViewEvents() - data class ShowPowerLevelValidation(val currentValue: Int, val newValue: Int) : RoomMemberProfileViewEvents() - data class ShowPowerLevelDemoteWarning(val currentValue: Int, val newValue: Int) : RoomMemberProfileViewEvents() + data class ShowPowerLevelValidation(val currentValue: UserPowerLevel, val newValue: UserPowerLevel.Value) : RoomMemberProfileViewEvents() + data class ShowPowerLevelDemoteWarning(val currentValue: UserPowerLevel, val newValue: UserPowerLevel.Value) : RoomMemberProfileViewEvents() data class StartVerification( val userId: String, diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt index 91024ee136..47d4cf423e 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -23,7 +23,6 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.lib.strings.CommonStrings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine @@ -42,9 +41,9 @@ import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm import org.matrix.android.sdk.api.session.room.model.RoomType -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem @@ -233,15 +232,15 @@ class RoomMemberProfileViewModel @AssistedInject constructor( if (room == null || action.previousValue == action.newValue) { return@withState } - val currentPowerLevelsContent = state.powerLevelsContent ?: return@withState - val myPowerLevel = PowerLevelsHelper(currentPowerLevelsContent).getUserPowerLevelValue(session.myUserId) + val roomPowerLevels = state.roomPowerLevels ?: return@withState + val myPowerLevel = roomPowerLevels.getUserPowerLevel(session.myUserId) if (action.askForValidation && action.newValue >= myPowerLevel) { _viewEvents.post(RoomMemberProfileViewEvents.ShowPowerLevelValidation(action.previousValue, action.newValue)) } else if (action.askForValidation && state.isMine) { _viewEvents.post(RoomMemberProfileViewEvents.ShowPowerLevelDemoteWarning(action.previousValue, action.newValue)) } else { - val newPowerLevelsContent = currentPowerLevelsContent - .setUserPowerLevel(state.userId, action.newValue) + val newPowerLevelsContent = (roomPowerLevels.powerLevelsContent ?: PowerLevelsContent()) + .setUserPowerLevel(state.userId, action.newValue.value) .toContent() viewModelScope.launch { _viewEvents.post(RoomMemberProfileViewEvents.Loading()) @@ -361,19 +360,17 @@ class RoomMemberProfileViewModel @AssistedInject constructor( private fun observeRoomSummaryAndPowerLevels(room: Room) { val roomSummaryLive = room.flow().liveRoomSummary().unwrap() - val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow() - - powerLevelsContentLive - .onEach { - val powerLevelsHelper = PowerLevelsHelper(it) + val powerLevelsFlow = room.flow().liveRoomPowerLevels() + powerLevelsFlow + .onEach { roomPowerLevels -> val permissions = ActionPermissions( - canKick = powerLevelsHelper.isUserAbleToKick(session.myUserId), - canBan = powerLevelsHelper.isUserAbleToBan(session.myUserId), - canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId), - canEditPowerLevel = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_POWER_LEVELS) + canKick = roomPowerLevels.isUserAbleToKick(session.myUserId), + canBan = roomPowerLevels.isUserAbleToBan(session.myUserId), + canInvite = roomPowerLevels.isUserAbleToInvite(session.myUserId), + canEditPowerLevel = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_POWER_LEVELS) ) setState { - copy(powerLevelsContent = it, actionPermissions = permissions) + copy(roomPowerLevels = roomPowerLevels, actionPermissions = permissions) } }.launchIn(viewModelScope) @@ -388,14 +385,14 @@ class RoomMemberProfileViewModel @AssistedInject constructor( copy(isRoomEncrypted = false) } } - roomSummaryLive.combine(powerLevelsContentLive) { roomSummary, powerLevelsContent -> + roomSummaryLive.combine(powerLevelsFlow) { roomSummary, roomPowerLevels -> val roomName = roomSummary.toMatrixItem().getBestName() - val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) - when (val userPowerLevel = powerLevelsHelper.getUserRole(initialState.userId)) { + when (roomPowerLevels.getSuggestedRole(initialState.userId)) { + Role.SuperAdmin, + Role.Creator -> stringProvider.getString(CommonStrings.room_member_power_level_owner_in, roomName) Role.Admin -> stringProvider.getString(CommonStrings.room_member_power_level_admin_in, roomName) Role.Moderator -> stringProvider.getString(CommonStrings.room_member_power_level_moderator_in, roomName) - Role.Default -> stringProvider.getString(CommonStrings.room_member_power_level_default_in, roomName) - is Role.Custom -> stringProvider.getString(CommonStrings.room_member_power_level_custom_in, userPowerLevel.value, roomName) + Role.User -> stringProvider.getString(CommonStrings.room_member_power_level_default_in, roomName) } }.execute { copy(userPowerLevelString = it) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt index 7d550c891d..a51eeef8e3 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt @@ -12,7 +12,7 @@ import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels import org.matrix.android.sdk.api.util.MatrixItem data class RoomMemberProfileViewState( @@ -24,7 +24,7 @@ data class RoomMemberProfileViewState( val isIgnored: Async = Uninitialized, val isRoomEncrypted: Boolean = false, val isAlgorithmSupported: Boolean = true, - val powerLevelsContent: PowerLevelsContent? = null, + val roomPowerLevels: RoomPowerLevels? = null, val userPowerLevelString: Async = Uninitialized, val userMatrixItem: Async = Uninitialized, val userMXCrossSigningInfo: MXCrossSigningInfo? = null, diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt index ffe6314592..c750b91f8f 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/powerlevel/EditPowerLevelDialogs.kt @@ -17,8 +17,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.databinding.DialogEditPowerLevelBinding +import im.vector.app.features.powerlevel.isOwner import im.vector.lib.strings.CommonStrings import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel object EditPowerLevelDialogs { @@ -26,46 +28,45 @@ object EditPowerLevelDialogs { fun showChoice( activity: Activity, @StringRes titleRes: Int, - currentRole: Role, - listener: (Int) -> Unit + currentPowerLevel: UserPowerLevel.Value, + listener: (UserPowerLevel.Value) -> Unit ) { val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_edit_power_level, null) val views = DialogEditPowerLevelBinding.bind(dialogLayout) - views.powerLevelRadioGroup.setOnCheckedChangeListener { _, checkedId -> - views.powerLevelCustomEditLayout.isVisible = checkedId == R.id.powerLevelCustomRadio - } - views.powerLevelCustomEdit.setText("${currentRole.value}") - + val currentRole = Role.getSuggestedRole(currentPowerLevel) when (currentRole) { + Role.Creator, + Role.SuperAdmin -> views.powerLevelOwnerRadio.isChecked = true Role.Admin -> views.powerLevelAdminRadio.isChecked = true Role.Moderator -> views.powerLevelModeratorRadio.isChecked = true - Role.Default -> views.powerLevelDefaultRadio.isChecked = true - else -> views.powerLevelCustomRadio.isChecked = true + Role.User -> views.powerLevelDefaultRadio.isChecked = true } - + views.powerLevelOwnerRadio.isVisible = currentRole.isOwner() MaterialAlertDialogBuilder(activity) .setTitle(titleRes) .setView(dialogLayout) .setPositiveButton(CommonStrings.edit) { _, _ -> val newValue = when (views.powerLevelRadioGroup.checkedRadioButtonId) { - R.id.powerLevelAdminRadio -> Role.Admin.value - R.id.powerLevelModeratorRadio -> Role.Moderator.value - R.id.powerLevelDefaultRadio -> Role.Default.value - else -> { - views.powerLevelCustomEdit.text?.toString()?.toInt() ?: currentRole.value - } + R.id.powerLevelOwnerRadio -> UserPowerLevel.SuperAdmin + R.id.powerLevelAdminRadio -> UserPowerLevel.Admin + R.id.powerLevelModeratorRadio -> UserPowerLevel.Moderator + R.id.powerLevelDefaultRadio -> UserPowerLevel.User + else -> null + } + if (newValue != null) { + listener(newValue) } - listener(newValue) } .setNegativeButton(CommonStrings.action_cancel, null) - .setOnKeyListener(DialogInterface.OnKeyListener - { dialog, keyCode, event -> - if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - dialog.cancel() - return@OnKeyListener true - } - false - }) + .setOnKeyListener( + DialogInterface.OnKeyListener + { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel() + return@OnKeyListener true + } + false + }) .setOnDismissListener { dialogLayout.hideKeyboard() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index 5bbfab6e18..db5cd3b9cb 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -9,6 +9,7 @@ package im.vector.app.features.roomprofile import android.content.Context import android.content.Intent +import android.view.View import android.widget.Toast import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks @@ -68,6 +69,9 @@ class RoomProfileActivity : override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java) roomProfileArgs = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG) ?: return diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 1a56de9fab..6c1f3b14fd 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -45,6 +45,7 @@ import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.app.features.navigation.SettingsActivityPayload +import im.vector.app.features.room.LeaveRoomPrompt import im.vector.lib.strings.CommonStrings import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -320,25 +321,16 @@ class RoomProfileFragment : } override fun onLeaveRoomClicked() { - val isPublicRoom = roomProfileViewModel.isPublicRoom() - val message = buildString { - append(getString(CommonStrings.room_participants_leave_prompt_msg)) - if (!isPublicRoom) { - append("\n\n") - append(getString(CommonStrings.room_participants_leave_private_warning)) + withState(roomProfileViewModel) { state -> + val warning = when { + state.isLastAdmin -> LeaveRoomPrompt.Warning.LAST_ADMIN + state.roomSummary()?.isPublic == false -> LeaveRoomPrompt.Warning.PRIVATE_ROOM + else -> LeaveRoomPrompt.Warning.NONE + } + LeaveRoomPrompt.show(requireContext(), warning) { + roomProfileViewModel.handle(RoomProfileAction.LeaveRoom) } } - MaterialAlertDialogBuilder( - requireContext(), - if (isPublicRoom) 0 else im.vector.lib.ui.styles.R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive - ) - .setTitle(CommonStrings.room_participants_leave_prompt_title) - .setMessage(message) - .setPositiveButton(CommonStrings.action_leave) { _, _ -> - roomProfileViewModel.handle(RoomProfileAction.LeaveRoom) - } - .setNegativeButton(CommonStrings.action_cancel, null) - .show() } override fun onRoomAliasesClicked() { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt index 83b1c5a950..eae4d47de6 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt @@ -19,7 +19,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.home.ShortcutCreator -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory +import im.vector.app.features.powerlevel.isLastAdminFlow import im.vector.app.features.session.coroutineScope import im.vector.lib.strings.CommonStrings import kotlinx.coroutines.Dispatchers @@ -39,7 +39,6 @@ import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.state.isPublic import org.matrix.android.sdk.flow.FlowRoom import org.matrix.android.sdk.flow.flow @@ -72,6 +71,14 @@ class RoomProfileViewModel @AssistedInject constructor( observePermissions() observePowerLevels() observeCryptoSettings(flowRoom) + observeIsLastAdmin() + } + + private fun observeIsLastAdmin() { + room.isLastAdminFlow(session.myUserId) + .onEach { isLastAdmin -> + setState { copy(isLastAdmin = isLastAdmin) } + }.launchIn(viewModelScope) } private fun observeCryptoSettings(flowRoom: FlowRoom) { @@ -113,11 +120,10 @@ class RoomProfileViewModel @AssistedInject constructor( } private fun observePowerLevels() { - val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow() + val powerLevelsContentLive = room.flow().liveRoomPowerLevels() powerLevelsContentLive - .onEach { - val powerLevelsHelper = PowerLevelsHelper(it) - val canUpdateRoomState = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) + .onEach { roomPowerLevels -> + val canUpdateRoomState = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) setState { copy(canUpdateRoomState = canUpdateRoomState) } @@ -156,12 +162,10 @@ class RoomProfileViewModel @AssistedInject constructor( } private fun observePermissions() { - PowerLevelsFlowFactory(room) - .createFlow() - .setOnEach { - val powerLevelsHelper = PowerLevelsHelper(it) + room.flow().liveRoomPowerLevels() + .setOnEach { roomPowerLevels -> val permissions = RoomProfileViewState.ActionPermissions( - canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) + canEnableEncryption = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION) ) copy(actionPermissions = permissions) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt index d6784dc8a2..580b5a1283 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt @@ -30,6 +30,7 @@ data class RoomProfileViewState( val encryptToVerifiedDeviceOnly: Async = Uninitialized, val globalCryptoConfig: Async = Uninitialized, val unverifiedDevicesInTheRoom: Async = Uninitialized, + val isLastAdmin: Boolean = false ) : MavericksState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt index a80e7b2050..2c1a112620 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasViewModel.kt @@ -18,7 +18,6 @@ import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -29,7 +28,6 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.mapOptional import org.matrix.android.sdk.flow.unwrap @@ -125,12 +123,10 @@ class RoomAliasViewModel @AssistedInject constructor( } private fun observePowerLevel() { - PowerLevelsFlowFactory(room) - .createFlow() - .onEach { - val powerLevelsHelper = PowerLevelsHelper(it) + room.flow().liveRoomPowerLevels() + .onEach { roomPowerLevels -> val permissions = RoomAliasViewState.ActionPermissions( - canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend( + canChangeCanonicalAlias = roomPowerLevels.isUserAllowedToSend( userId = session.myUserId, isState = true, eventType = EventType.STATE_ROOM_CANONICAL_ALIAS diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt index df697bd501..4d373dfaa8 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewModel.kt @@ -15,7 +15,6 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.lib.strings.CommonStrings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -29,7 +28,6 @@ import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.unwrap @@ -62,12 +60,10 @@ class RoomBannedMemberListViewModel @AssistedInject constructor( ) } - val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow() - - powerLevelsContentLive - .setOnEach { - val powerLevelsHelper = PowerLevelsHelper(it) - copy(canUserBan = powerLevelsHelper.isUserAbleToBan(session.myUserId)) + val powerLevelsFlow = room.flow().liveRoomPowerLevels() + powerLevelsFlow + .setOnEach { roomPowerLevels -> + copy(canUserBan = roomPowerLevels.isUserAbleToBan(session.myUserId)) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListComparator.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListComparator.kt new file mode 100644 index 0000000000..ffb4eaf522 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListComparator.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2020-2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package im.vector.app.features.roomprofile.members + +import javax.inject.Inject + +class RoomMemberListComparator @Inject constructor() : Comparator { + + override fun compare(leftRoomMember: RoomMemberWithPowerLevel?, rightRoomMember: RoomMemberWithPowerLevel?): Int { + return when (leftRoomMember) { + null -> + when (rightRoomMember) { + null -> 0 + else -> 1 + } + else -> + when (rightRoomMember) { + null -> -1 + else -> + when { + leftRoomMember.powerLevel > rightRoomMember.powerLevel -> -1 + leftRoomMember.powerLevel < rightRoomMember.powerLevel -> 1 + leftRoomMember.summary.displayName.isNullOrBlank() -> + when { + rightRoomMember.summary.displayName.isNullOrBlank() -> { + // No display names, compare ids + leftRoomMember.summary.userId.compareTo(rightRoomMember.summary.userId) + } + else -> 1 + } + else -> + when { + rightRoomMember.summary.displayName.isNullOrBlank() -> -1 + else -> { + when (leftRoomMember.summary.displayName) { + rightRoomMember.summary.displayName -> + // Same display name, compare id + leftRoomMember.summary.userId.compareTo(rightRoomMember.summary.userId) + else -> + leftRoomMember.summary.displayName!!.compareTo(rightRoomMember.summary.displayName!!, true) + } + } + } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index 88f55aac70..24d2dddc8e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -16,11 +16,13 @@ import im.vector.app.core.extensions.join import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.roomprofile.permissions.RoleFormatter import me.gujun.android.span.span import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent +import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -29,7 +31,8 @@ class RoomMemberListController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, private val colorProvider: ColorProvider, - private val roomMemberSummaryFilter: RoomMemberSummaryFilter + private val roomMemberSummaryFilter: RoomMemberSummaryFilter, + private val roleFormatter: RoleFormatter, ) : TypedEpoxyController() { interface Callback { @@ -56,13 +59,13 @@ class RoomMemberListController @Inject constructor( .orEmpty() var threePidInvitesDone = filteredThreePidInvites.isEmpty() - for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) { - val filteredRoomMemberList = roomMemberList.filter { roomMemberSummaryFilter.test(it) } + for ((category, roomMemberList) in roomMembersByPowerLevel) { + val filteredRoomMemberList = roomMemberList.filter { roomMemberSummaryFilter.test(it.summary) } if (filteredRoomMemberList.isEmpty()) { continue } - if (powerLevelCategory == RoomMemberListCategories.USER && !threePidInvitesDone) { + if (category == RoomMemberListCategories.USER && !threePidInvitesDone) { // If there is no regular invite, display threepid invite before the regular user buildProfileSection( stringProvider.getString(RoomMemberListCategories.INVITE.titleRes) @@ -73,20 +76,20 @@ class RoomMemberListController @Inject constructor( } buildProfileSection( - stringProvider.getString(powerLevelCategory.titleRes) + stringProvider.getString(category.titleRes) ) filteredRoomMemberList.join( each = { _, roomMember -> - buildRoomMember(roomMember, powerLevelCategory, host, data) + buildRoomMember(roomMember, host, data) }, between = { _, roomMemberBefore -> dividerItem { - id("divider_${roomMemberBefore.userId}") + id("divider_${roomMemberBefore.summary.userId}") } } ) - if (powerLevelCategory == RoomMemberListCategories.INVITE && !threePidInvitesDone) { + if (category == RoomMemberListCategories.INVITE && !threePidInvitesDone) { // Display the threepid invite after the regular invite dividerItem { id("divider_threepidinvites") @@ -108,24 +111,24 @@ class RoomMemberListController @Inject constructor( } private fun buildRoomMember( - roomMember: RoomMemberSummary, - powerLevelCategory: RoomMemberListCategories, + roomMember: RoomMemberWithPowerLevel, host: RoomMemberListController, data: RoomMemberListViewState ) { - val powerLabel = stringProvider.getString(powerLevelCategory.titleRes) + val role = Role.getSuggestedRole(roomMember.powerLevel) + val powerLabel = roleFormatter.format(role) profileMatrixItemWithPowerLevelWithPresence { - id(roomMember.userId) - matrixItem(roomMember.toMatrixItem()) + id(roomMember.summary.userId) + matrixItem(roomMember.summary.toMatrixItem()) avatarRenderer(host.avatarRenderer) - userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) + userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.summary.userId)) clickListener { - host.callback?.onRoomMemberClicked(roomMember) + host.callback?.onRoomMemberClicked(roomMember.summary) } showPresence(true) - userPresence(roomMember.userPresence) - ignoredUser(roomMember.userId in data.ignoredUserIds) + userPresence(roomMember.summary.userPresence) + ignoredUser(roomMember.summary.userId in data.ignoredUserIds) powerLevelLabel( span { span(powerLabel) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 465da6a9c2..65e02f63d4 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -16,7 +16,6 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -31,22 +30,19 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.flow.mapOptional import org.matrix.android.sdk.flow.unwrap import timber.log.Timber class RoomMemberListViewModel @AssistedInject constructor( @Assisted initialState: RoomMemberListViewState, - private val roomMemberSummaryComparator: RoomMemberSummaryComparator, + private val roomMemberListComparator: RoomMemberListComparator, private val session: Session ) : VectorViewModel(initialState) { @@ -75,14 +71,12 @@ class RoomMemberListViewModel @AssistedInject constructor( memberships = Membership.activeMemberships() } + val powerLevelsFlow = room.flow().liveRoomPowerLevels() combine( roomFlow.liveRoomMembers(roomMemberQueryParams), - roomFlow - .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - .mapOptional { it.content.toModel() } - .unwrap() - ) { roomMembers, powerLevelsContent -> - buildRoomMemberSummaries(powerLevelsContent, roomMembers) + powerLevelsFlow, + ) { roomMembers, roomPowerLevels -> + buildRoomMemberSummaries(roomPowerLevels, roomMembers) } .execute { async -> copy(roomMemberSummaries = async) @@ -142,11 +136,11 @@ class RoomMemberListViewModel @AssistedInject constructor( } private fun observePowerLevel() { - PowerLevelsFlowFactory(room).createFlow() - .onEach { + room.flow().liveRoomPowerLevels() + .onEach { roomPowerLevels -> val permissions = ActionPermissions( - canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId), - canRevokeThreePidInvite = PowerLevelsHelper(it).isUserAllowedToSend( + canInvite = roomPowerLevels.isUserAbleToInvite(session.myUserId), + canRevokeThreePidInvite = roomPowerLevels.isUserAllowedToSend( userId = session.myUserId, isState = true, eventType = EventType.STATE_ROOM_THIRD_PARTY_INVITE @@ -184,31 +178,34 @@ class RoomMemberListViewModel @AssistedInject constructor( } } - private fun buildRoomMemberSummaries(powerLevelsContent: PowerLevelsContent, roomMembers: List): RoomMemberSummaries { - val admins = ArrayList() - val moderators = ArrayList() - val users = ArrayList(roomMembers.size) - val customs = ArrayList() - val invites = ArrayList() - val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) + private fun buildRoomMemberSummaries(roomPowerLevels: RoomPowerLevels, roomMembers: List): RoomMembersByRole { + val admins = ArrayList() + val moderators = ArrayList() + val users = ArrayList(roomMembers.size) + val invites = ArrayList() roomMembers .forEach { roomMember -> - val userRole = powerLevelsHelper.getUserRole(roomMember.userId) + val powerLevel = roomPowerLevels.getUserPowerLevel(roomMember.userId) + val userRole = Role.getSuggestedRole(powerLevel) + val roomMemberWithPowerLevel = RoomMemberWithPowerLevel( + powerLevel = powerLevel, + summary = roomMember, + ) when { - roomMember.membership == Membership.INVITE -> invites.add(roomMember) - userRole == Role.Admin -> admins.add(roomMember) - userRole == Role.Moderator -> moderators.add(roomMember) - userRole == Role.Default -> users.add(roomMember) - else -> customs.add(roomMember) + roomMember.membership == Membership.INVITE -> invites.add(roomMemberWithPowerLevel) + userRole == Role.SuperAdmin || + userRole == Role.Creator || + userRole == Role.Admin -> admins.add(roomMemberWithPowerLevel) + userRole == Role.Moderator -> moderators.add(roomMemberWithPowerLevel) + userRole == Role.User -> users.add(roomMemberWithPowerLevel) } } return listOf( - RoomMemberListCategories.ADMIN to admins.sortedWith(roomMemberSummaryComparator), - RoomMemberListCategories.MODERATOR to moderators.sortedWith(roomMemberSummaryComparator), - RoomMemberListCategories.CUSTOM to customs.sortedWith(roomMemberSummaryComparator), - RoomMemberListCategories.INVITE to invites.sortedWith(roomMemberSummaryComparator), - RoomMemberListCategories.USER to users.sortedWith(roomMemberSummaryComparator) + RoomMemberListCategories.ADMIN to admins.sortedWith(roomMemberListComparator), + RoomMemberListCategories.MODERATOR to moderators.sortedWith(roomMemberListComparator), + RoomMemberListCategories.INVITE to invites.sortedWith(roomMemberListComparator), + RoomMemberListCategories.USER to users.sortedWith(roomMemberListComparator) ) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt index 90da131cb5..d68a9fceba 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt @@ -18,11 +18,12 @@ import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel data class RoomMemberListViewState( val roomId: String, val roomSummary: Async = Uninitialized, - val roomMemberSummaries: Async = Uninitialized, + val roomMemberSummaries: Async = Uninitialized, val areAllMembersLoaded: Boolean = false, val ignoredUserIds: List = emptyList(), val filter: String = "", @@ -41,12 +42,16 @@ data class ActionPermissions( val canRevokeThreePidInvite: Boolean = false ) -typealias RoomMemberSummaries = List>> +data class RoomMemberWithPowerLevel( + val powerLevel: UserPowerLevel, + val summary: RoomMemberSummary, +) + +typealias RoomMembersByRole = List>> enum class RoomMemberListCategories(@StringRes val titleRes: Int) { ADMIN(CommonStrings.room_member_power_level_admins), MODERATOR(CommonStrings.room_member_power_level_moderators), - CUSTOM(CommonStrings.room_member_power_level_custom), INVITE(CommonStrings.room_member_power_level_invites), USER(CommonStrings.room_member_power_level_users) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryComparator.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryComparator.kt deleted file mode 100644 index fbff99b287..0000000000 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryComparator.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2020-2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package im.vector.app.features.roomprofile.members - -import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -import javax.inject.Inject - -class RoomMemberSummaryComparator @Inject constructor() : Comparator { - - override fun compare(leftRoomMemberSummary: RoomMemberSummary?, rightRoomMemberSummary: RoomMemberSummary?): Int { - return when (leftRoomMemberSummary) { - null -> - when (rightRoomMemberSummary) { - null -> 0 - else -> 1 - } - else -> - when (rightRoomMemberSummary) { - null -> -1 - else -> - when { - leftRoomMemberSummary.displayName.isNullOrBlank() -> - when { - rightRoomMemberSummary.displayName.isNullOrBlank() -> { - // No display names, compare ids - leftRoomMemberSummary.userId.compareTo(rightRoomMemberSummary.userId) - } - else -> 1 - } - else -> - when { - rightRoomMemberSummary.displayName.isNullOrBlank() -> -1 - else -> { - when (leftRoomMemberSummary.displayName) { - rightRoomMemberSummary.displayName -> - // Same display name, compare id - leftRoomMemberSummary.userId.compareTo(rightRoomMemberSummary.userId) - else -> - leftRoomMemberSummary.displayName!!.compareTo(rightRoomMemberSummary.displayName!!, true) - } - } - } - } - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoleFormatter.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoleFormatter.kt index 6bc62cb53d..4febb822ad 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoleFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoleFormatter.kt @@ -19,8 +19,9 @@ class RoleFormatter @Inject constructor( return when (role) { Role.Admin -> stringProvider.getString(CommonStrings.power_level_admin) Role.Moderator -> stringProvider.getString(CommonStrings.power_level_moderator) - Role.Default -> stringProvider.getString(CommonStrings.power_level_default) - is Role.Custom -> stringProvider.getString(CommonStrings.power_level_custom, role.value) + Role.User -> stringProvider.getString(CommonStrings.power_level_default) + Role.Creator -> stringProvider.getString(CommonStrings.power_level_owner) + Role.SuperAdmin -> stringProvider.getString(CommonStrings.power_level_owner) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsAction.kt index a8eb77bd03..cb4376c98f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsAction.kt @@ -8,9 +8,10 @@ package im.vector.app.features.roomprofile.permissions import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel sealed class RoomPermissionsAction : VectorViewModelAction { object ToggleShowAllPermissions : RoomPermissionsAction() - data class UpdatePermission(val editablePermission: EditablePermission, val powerLevel: Int) : RoomPermissionsAction() + data class UpdatePermission(val editablePermission: EditablePermission, val powerLevel: UserPowerLevel.Value) : RoomPermissionsAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt index 2e5d89e409..78a405db49 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsController.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.redactOrDefault import org.matrix.android.sdk.api.session.room.model.stateDefaultOrDefault import org.matrix.android.sdk.api.session.room.model.usersDefaultOrDefault import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel import javax.inject.Inject class RoomPermissionsController @Inject constructor( @@ -34,7 +35,7 @@ class RoomPermissionsController @Inject constructor( ) : TypedEpoxyController() { interface Callback { - fun onEditPermission(editablePermission: EditablePermission, currentRole: Role) + fun onEditPermission(editablePermission: EditablePermission, currentPowerLevel: UserPowerLevel.Value) fun toggleShowAllPermissions() } @@ -165,7 +166,8 @@ class RoomPermissionsController @Inject constructor( editable: Boolean, isSpace: Boolean ) { - val currentRole = getCurrentRole(editablePermission, content) + val currentPowerLevel = getPowerLevel(editablePermission, content) + val currentRole = Role.getSuggestedRole(currentPowerLevel) buildProfileAction( id = editablePermission.labelResId.toString(), title = stringProvider.getString( @@ -177,12 +179,12 @@ class RoomPermissionsController @Inject constructor( action = { callback ?.takeIf { editable } - ?.onEditPermission(editablePermission, currentRole) + ?.onEditPermission(editablePermission, currentPowerLevel) } ) } - private fun getCurrentRole(editablePermission: EditablePermission, content: PowerLevelsContent): Role { + private fun getPowerLevel(editablePermission: EditablePermission, content: PowerLevelsContent): UserPowerLevel.Value { val value = when (editablePermission) { is EditablePermission.EventTypeEditablePermission -> content.events?.get(editablePermission.eventType) ?: content.stateDefaultOrDefault() is EditablePermission.DefaultRole -> content.usersDefaultOrDefault() @@ -194,20 +196,6 @@ class RoomPermissionsController @Inject constructor( is EditablePermission.RemoveMessagesSentByOthers -> content.redactOrDefault() is EditablePermission.NotifyEveryone -> content.notificationLevel(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY) } - - return Role.fromValue( - value, - when (editablePermission) { - is EditablePermission.EventTypeEditablePermission -> content.stateDefaultOrDefault() - is EditablePermission.DefaultRole -> Role.Default.value - is EditablePermission.SendMessages -> Role.Default.value - is EditablePermission.InviteUsers -> Role.Moderator.value - is EditablePermission.ChangeSettings -> Role.Moderator.value - is EditablePermission.KickUsers -> Role.Moderator.value - is EditablePermission.BanUsers -> Role.Moderator.value - is EditablePermission.RemoveMessagesSentByOthers -> Role.Moderator.value - is EditablePermission.NotifyEveryone -> Role.Moderator.value - } - ) + return UserPowerLevel.Value(value) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt index 5d92a96b5f..d2e94bf504 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsFragment.kt @@ -26,7 +26,7 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs import im.vector.app.features.roomprofile.RoomProfileArgs import im.vector.lib.strings.CommonStrings -import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -93,8 +93,8 @@ class RoomPermissionsFragment : } } - override fun onEditPermission(editablePermission: EditablePermission, currentRole: Role) { - EditPowerLevelDialogs.showChoice(requireActivity(), editablePermission.labelResId, currentRole) { newPowerLevel -> + override fun onEditPermission(editablePermission: EditablePermission, currentPowerLevel: UserPowerLevel.Value) { + EditPowerLevelDialogs.showChoice(requireActivity(), editablePermission.labelResId, currentPowerLevel) { newPowerLevel -> viewModel.handle(RoomPermissionsAction.UpdatePermission(editablePermission, newPowerLevel)) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt index 0b942d78cb..7f88ae88c7 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/permissions/RoomPermissionsViewModel.kt @@ -9,13 +9,13 @@ package im.vector.app.features.roomprofile.permissions import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -24,7 +24,6 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.unwrap @@ -59,21 +58,24 @@ class RoomPermissionsViewModel @AssistedInject constructor( } private fun observePowerLevel() { - PowerLevelsFlowFactory(room) - .createFlow() - .onEach { powerLevelContent -> - val powerLevelsHelper = PowerLevelsHelper(powerLevelContent) + room.flow().liveRoomPowerLevels() + .onEach { roomPowerLevels -> val permissions = RoomPermissionsViewState.ActionPermissions( - canChangePowerLevels = powerLevelsHelper.isUserAllowedToSend( + canChangePowerLevels = roomPowerLevels.isUserAllowedToSend( userId = session.myUserId, isState = true, eventType = EventType.STATE_ROOM_POWER_LEVELS ) ) + val powerLevelsContent = roomPowerLevels.powerLevelsContent setState { copy( actionPermissions = permissions, - currentPowerLevelsContent = Success(powerLevelContent) + currentPowerLevelsContent = if (powerLevelsContent != null) { + Success(powerLevelsContent) + } else { + Uninitialized + } ) } }.launchIn(viewModelScope) @@ -94,26 +96,26 @@ class RoomPermissionsViewModel @AssistedInject constructor( private fun updatePermission(action: RoomPermissionsAction.UpdatePermission) { withState { state -> - val currentPowerLevel = state.currentPowerLevelsContent.invoke() ?: return@withState + val currentPowerLevelsContent = state.currentPowerLevelsContent.invoke() ?: return@withState postLoading(true) viewModelScope.launch { try { val newPowerLevelsContent = when (action.editablePermission) { - is EditablePermission.EventTypeEditablePermission -> currentPowerLevel.copy( - events = currentPowerLevel.events.orEmpty().toMutableMap().apply { - put(action.editablePermission.eventType, action.powerLevel) + is EditablePermission.EventTypeEditablePermission -> currentPowerLevelsContent.copy( + events = currentPowerLevelsContent.events.orEmpty().toMutableMap().apply { + put(action.editablePermission.eventType, action.powerLevel.value) } ) - is EditablePermission.DefaultRole -> currentPowerLevel.copy(usersDefault = action.powerLevel) - is EditablePermission.SendMessages -> currentPowerLevel.copy(eventsDefault = action.powerLevel) - is EditablePermission.InviteUsers -> currentPowerLevel.copy(invite = action.powerLevel) - is EditablePermission.ChangeSettings -> currentPowerLevel.copy(stateDefault = action.powerLevel) - is EditablePermission.KickUsers -> currentPowerLevel.copy(kick = action.powerLevel) - is EditablePermission.BanUsers -> currentPowerLevel.copy(ban = action.powerLevel) - is EditablePermission.RemoveMessagesSentByOthers -> currentPowerLevel.copy(redact = action.powerLevel) - is EditablePermission.NotifyEveryone -> currentPowerLevel.copy( - notifications = currentPowerLevel.notifications.orEmpty().toMutableMap().apply { - put(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY, action.powerLevel) + is EditablePermission.DefaultRole -> currentPowerLevelsContent.copy(usersDefault = action.powerLevel.value) + is EditablePermission.SendMessages -> currentPowerLevelsContent.copy(eventsDefault = action.powerLevel.value) + is EditablePermission.InviteUsers -> currentPowerLevelsContent.copy(invite = action.powerLevel.value) + is EditablePermission.ChangeSettings -> currentPowerLevelsContent.copy(stateDefault = action.powerLevel.value) + is EditablePermission.KickUsers -> currentPowerLevelsContent.copy(kick = action.powerLevel.value) + is EditablePermission.BanUsers -> currentPowerLevelsContent.copy(ban = action.powerLevel.value) + is EditablePermission.RemoveMessagesSentByOthers -> currentPowerLevelsContent.copy(redact = action.powerLevel.value) + is EditablePermission.NotifyEveryone -> currentPowerLevelsContent.copy( + notifications = currentPowerLevelsContent.notifications.orEmpty().toMutableMap().apply { + put(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY, action.powerLevel.value) } ) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailActivity.kt index dc3db0401e..a8a1c31862 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/detail/ui/RoomPollDetailActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.roomprofile.polls.detail.ui import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import com.airbnb.mvrx.Mavericks import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment @@ -25,6 +26,11 @@ class RoomPollDetailActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + override fun getCoordinatorLayout() = views.coordinatorLayout + + override val rootView: View + get() = views.coordinatorLayout + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt index af043b9ab2..c603927d77 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -15,7 +15,6 @@ import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapNotNull @@ -32,7 +31,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.mapOptional import org.matrix.android.sdk.flow.unwrap @@ -115,28 +113,26 @@ class RoomSettingsViewModel @AssistedInject constructor( ) } - val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow() - - powerLevelsContentLive - .onEach { - val powerLevelsHelper = PowerLevelsHelper(it) + val powerLevelsFlow = room.flow().liveRoomPowerLevels() + powerLevelsFlow + .onEach { roomPowerLevels -> val permissions = RoomSettingsViewState.ActionPermissions( - canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR), - canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME), - canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC), - canChangeHistoryVisibility = powerLevelsHelper.isUserAllowedToSend( + canChangeAvatar = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR), + canChangeName = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME), + canChangeTopic = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC), + canChangeHistoryVisibility = roomPowerLevels.isUserAllowedToSend( session.myUserId, true, EventType.STATE_ROOM_HISTORY_VISIBILITY ), - canChangeJoinRule = powerLevelsHelper.isUserAllowedToSend( + canChangeJoinRule = roomPowerLevels.isUserAllowedToSend( session.myUserId, true, EventType.STATE_ROOM_JOIN_RULES ) && - powerLevelsHelper.isUserAllowedToSend( + roomPowerLevels.isUserAllowedToSend( session.myUserId, true, EventType.STATE_ROOM_GUEST_ACCESS ), - canAddChildren = powerLevelsHelper.isUserAllowedToSend( + canAddChildren = roomPowerLevels.isUserAllowedToSend( session.myUserId, true, EventType.STATE_SPACE_CHILD ) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt index 30d2e688b6..868564737f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/joinrule/RoomJoinRuleActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.roomprofile.settings.joinrule import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import androidx.core.view.isVisible import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading @@ -40,6 +41,11 @@ class RoomJoinRuleActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + override fun getCoordinatorLayout() = views.coordinatorLayout + + override val rootView: View + get() = views.coordinatorLayout + private lateinit var roomProfileArgs: RoomProfileArgs val viewModel: RoomJoinRuleChooseRestrictedViewModel by viewModel() diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt index 62d0917490..c942d8f06a 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt @@ -11,6 +11,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable +import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.preference.Preference @@ -47,6 +48,9 @@ class VectorSettingsActivity : VectorBaseActivity override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + override fun getTitleRes() = CommonStrings.title_activity_settings private var keyToHighlight: String? = null diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt index 05732508b2..d786bf8a60 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceVerificationInfoBottomSheetController.kt @@ -17,6 +17,7 @@ import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.views.toDrawableRes import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem +import im.vector.app.features.settings.VectorPreferences import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.strings.CommonStrings import org.matrix.android.sdk.api.extensions.orFalse @@ -26,7 +27,8 @@ import javax.inject.Inject class DeviceVerificationInfoBottomSheetController @Inject constructor( private val stringProvider: StringProvider, - private val colorProvider: ColorProvider + private val colorProvider: ColorProvider, + private val vectorPreferences: VectorPreferences ) : TypedEpoxyController() { @@ -244,17 +246,19 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor( private fun addVerifyActions(cryptoDeviceInfo: CryptoDeviceInfo) { val host = this - bottomSheetDividerItem { - id("verifyDiv") - } - bottomSheetVerificationActionItem { - id("verify_text") - title(host.stringProvider.getString(CommonStrings.cross_signing_verify_by_text)) - titleColor(host.colorProvider.getColorFromAttribute(com.google.android.material.R.attr.colorPrimary)) - iconRes(R.drawable.ic_arrow_right) - iconColor(host.colorProvider.getColorFromAttribute(com.google.android.material.R.attr.colorPrimary)) - listener { - host.callback?.onAction(DevicesAction.VerifyMyDeviceManually(cryptoDeviceInfo.deviceId)) + if (vectorPreferences.developerMode()) { + bottomSheetDividerItem { + id("verifyDiv") + } + bottomSheetVerificationActionItem { + id("verify_text") + title(host.stringProvider.getString(CommonStrings.cross_signing_verify_by_text)) + titleColor(host.colorProvider.getColorFromAttribute(com.google.android.material.R.attr.colorPrimary)) + iconRes(R.drawable.ic_arrow_right) + iconColor(host.colorProvider.getColorFromAttribute(com.google.android.material.R.attr.colorPrimary)) + listener { + host.callback?.onAction(DevicesAction.VerifyMyDeviceManually(cryptoDeviceInfo.deviceId)) + } } } bottomSheetDividerItem { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionActivity.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionActivity.kt index 3625dee311..7a556e18eb 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionActivity.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.settings.devices.v2.rename import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import android.view.WindowManager import com.airbnb.mvrx.Mavericks import dagger.hilt.android.AndroidEntryPoint @@ -26,6 +27,11 @@ class RenameSessionActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + override fun getCoordinatorLayout() = views.coordinatorLayout + + override val rootView: View + get() = views.coordinatorLayout + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingActivity.kt b/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingActivity.kt index d2e8ffccbf..1ffb93d840 100644 --- a/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingActivity.kt +++ b/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingActivity.kt @@ -7,6 +7,7 @@ package im.vector.app.features.settings.font +import android.view.View import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.VectorBaseActivity @@ -17,6 +18,11 @@ class FontScaleSettingActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + override fun getCoordinatorLayout() = views.coordinatorLayout + + override val rootView: View + get() = views.coordinatorLayout + override fun initUiAndData() { if (isFirstCreation()) { addFragment(views.simpleFragmentContainer, FontScaleSettingFragment::class.java) diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt index e2a00c516f..3d89a6ebc9 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt @@ -9,6 +9,7 @@ package im.vector.app.features.share import android.content.Intent import android.os.Bundle +import android.view.View import com.airbnb.mvrx.viewModel import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment @@ -46,6 +47,9 @@ class IncomingShareActivity : VectorBaseActivity() { override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + private fun handleAppStarted() { // If we are not logged in, stop the sharing process and open login screen. // In the future, we might want to relaunch the sharing process after login. diff --git a/vector/src/main/java/im/vector/app/features/signout/hard/SignedOutActivity.kt b/vector/src/main/java/im/vector/app/features/signout/hard/SignedOutActivity.kt index c94d9ab7b9..e6b3b817ea 100644 --- a/vector/src/main/java/im/vector/app/features/signout/hard/SignedOutActivity.kt +++ b/vector/src/main/java/im/vector/app/features/signout/hard/SignedOutActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.signout.hard import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySignedOutBinding @@ -26,6 +27,9 @@ class SignedOutActivity : VectorBaseActivity() { override fun getBinding() = ActivitySignedOutBinding.inflate(layoutInflater) + override val rootView: View + get() = views.signedOut + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt index 5f060adeb9..7e5d776ca2 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceExploreActivity.kt @@ -11,6 +11,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.Mavericks @@ -38,6 +39,11 @@ class SpaceExploreActivity : VectorBaseActivity(), Matrix override fun getBinding(): ActivitySimpleBinding = ActivitySimpleBinding.inflate(layoutInflater) + override fun getCoordinatorLayout() = views.coordinatorLayout + + override val rootView: View + get() = views.coordinatorLayout + override fun getTitleRes(): Int = CommonStrings.space_explore_activity_title val sharedViewModel: SpaceDirectoryViewModel by viewModel() diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt index 404cfac9de..6744548872 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt @@ -20,7 +20,6 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -32,7 +31,6 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.flow.flow @@ -72,22 +70,20 @@ class SpaceMenuViewModel @AssistedInject constructor( } }.launchIn(viewModelScope) - PowerLevelsFlowFactory(room) - .createFlow() - .onEach { - val powerLevelsHelper = PowerLevelsHelper(it) + room.flow().liveRoomPowerLevels() + .onEach { roomPowerLevels -> - val canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId) - val canAddChild = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_SPACE_CHILD) + val canInvite = roomPowerLevels.isUserAbleToInvite(session.myUserId) + val canAddChild = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_SPACE_CHILD) - val canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR) - val canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME) - val canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC) + val canChangeAvatar = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR) + val canChangeName = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME) + val canChangeTopic = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC) - val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin + val isAdmin = roomPowerLevels.getSuggestedRole(session.myUserId) == Role.Admin val otherAdminCount = roomSummary?.otherMemberIds - ?.map { powerLevelsHelper.getUserRole(it) } - ?.count { it is Role.Admin } + ?.map { roomPowerLevels.getSuggestedRole(it) } + ?.count { it == Role.Admin } ?: 0 val isLastAdmin = isAdmin && otherAdminCount == 0 diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt index d698f2ed35..6b60253b4a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacePreviewActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.spaces import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Mavericks import dagger.hilt.android.AndroidEntryPoint @@ -29,6 +30,11 @@ class SpacePreviewActivity : VectorBaseActivity() { override fun getBinding(): ActivitySimpleBinding = ActivitySimpleBinding.inflate(layoutInflater) + override fun getCoordinatorLayout() = views.coordinatorLayout + + override val rootView: View + get() = views.coordinatorLayout + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) sharedActionViewModel = viewModelProvider.get(SpacePreviewSharedActionViewModel::class.java) diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt index bf91be4fa0..f12ab7b938 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt @@ -26,7 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset -import org.matrix.android.sdk.api.session.room.powerlevels.Role +import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel import org.matrix.android.sdk.api.session.space.CreateSpaceParams import timber.log.Timber import javax.inject.Inject @@ -65,7 +65,7 @@ class CreateSpaceViewModelTask @Inject constructor( if (params.isPublic) { this.roomAliasName = params.spaceAlias this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy( - invite = Role.Default.value + invite = UserPowerLevel.User.value ) this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE @@ -79,7 +79,7 @@ class CreateSpaceViewModelTask @Inject constructor( } ) this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy( - invite = Role.Moderator.value + invite = UserPowerLevel.Moderator.value ) } }) diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt index ae112908fc..ed73ace342 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt @@ -21,7 +21,6 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -36,7 +35,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.flow.flow import timber.log.Timber @@ -96,16 +94,14 @@ class SpaceDirectoryViewModel @AssistedInject constructor( private fun observePermissions() { val room = session.getRoom(initialState.spaceId) ?: return - val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow() + val powerLevelsFlow = room.flow().liveRoomPowerLevels() - powerLevelsContentLive - .onEach { - val powerLevelsHelper = PowerLevelsHelper(it) + powerLevelsFlow + .onEach { roomPowerLevels -> setState { copy( - canAddRooms = powerLevelsHelper.isUserAllowedToSend( - session.myUserId, true, - EventType.STATE_SPACE_CHILD + canAddRooms = roomPowerLevels.isUserAllowedToSend( + userId = session.myUserId, isState = true, eventType = EventType.STATE_SPACE_CHILD ) ) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt index cf7b706adf..84c3c78b2a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.spaces.leave import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.mvrx.Fail @@ -33,6 +34,11 @@ class SpaceLeaveAdvancedActivity : VectorBaseActivity setupToolbar(views.toolbar) .setSubtitle(state.spaceSummary?.name) .allowBack() - - state.spaceSummary?.let { summary -> - val warningMessage: CharSequence? = when { - summary.otherMemberIds.isEmpty() -> getString(CommonStrings.space_leave_prompt_msg_only_you) - state.isLastAdmin -> getString(CommonStrings.space_leave_prompt_msg_as_admin) - !summary.isPublic -> getString(CommonStrings.space_leave_prompt_msg_private) - else -> null - } - - views.spaceLeavePromptDescription.isVisible = warningMessage != null - views.spaceLeavePromptDescription.text = warningMessage - } - - views.spaceLeavePromptTitle.text = getString(CommonStrings.space_leave_prompt_msg_with_name, state.spaceSummary?.name ?: "") } views.roomList.configureWith(controller) @@ -107,6 +92,19 @@ class SpaceLeaveAdvancedFragment : override fun invalidate() = withState(viewModel) { state -> super.invalidate() + state.spaceSummary?.let { summary -> + val warningMessage: CharSequence? = when { + summary.otherMemberIds.isEmpty() -> getString(CommonStrings.space_leave_prompt_msg_only_you) + state.isLastAdmin -> getString(CommonStrings.space_leave_prompt_msg_as_admin) + !summary.isPublic -> getString(CommonStrings.space_leave_prompt_msg_private) + else -> null + } + views.spaceLeavePromptDescription.isVisible = warningMessage != null + views.spaceLeavePromptDescription.text = warningMessage + } + + views.spaceLeavePromptTitle.text = getString(CommonStrings.space_leave_prompt_msg_with_name, state.spaceSummary?.name ?: "") + if (state.isFilteringEnabled) { views.appBarLayout.setExpanded(false) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt index de68b697ef..7f0a2d4008 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt @@ -20,22 +20,16 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.powerlevel.isLastAdminFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import okhttp3.internal.toImmutableList -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.SpaceFilter import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper -import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.unwrap @@ -49,22 +43,13 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor( init { val space = session.getRoom(initialState.spaceId) + + space?.isLastAdminFlow(session.myUserId) + ?.onEach { isLastAdmin -> + setState { copy(isLastAdmin = isLastAdmin) } + }?.launchIn(viewModelScope) + val spaceSummary = space?.roomSummary() - - val powerLevelsEvent = space?.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - powerLevelsEvent?.content?.toModel()?.let { powerLevelsContent -> - val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) - val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin - val otherAdminCount = spaceSummary?.otherMemberIds - ?.map { powerLevelsHelper.getUserRole(it) } - ?.count { it is Role.Admin } - ?: 0 - val isLastAdmin = isAdmin && otherAdminCount == 0 - setState { - copy(isLastAdmin = isLastAdmin) - } - } - setState { copy(spaceSummary = spaceSummary) } session.getRoom(initialState.spaceId) ?.flow() diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt index 7296b8df3c..fbdc94109d 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt @@ -11,6 +11,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable +import android.view.View import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope @@ -49,6 +50,11 @@ class SpaceManageActivity : VectorBaseActivity() { override fun getBinding(): ActivitySimpleLoadingBinding = ActivitySimpleLoadingBinding.inflate(layoutInflater) + override fun getCoordinatorLayout() = views.coordinatorLayout + + override val rootView: View + get() = views.coordinatorLayout + override fun getTitleRes(): Int = CommonStrings.space_add_existing_rooms val sharedViewModel: SpaceManageSharedViewModel by viewModel() diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt index 7a12dedd56..4bb2f0c44a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleActivity.kt @@ -10,6 +10,7 @@ package im.vector.app.features.spaces.people import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope @@ -32,6 +33,11 @@ class SpacePeopleActivity : VectorBaseActivity() { override fun getBinding() = ActivitySimpleLoadingBinding.inflate(layoutInflater) + override fun getCoordinatorLayout() = views.coordinatorLayout + + override val rootView: View + get() = views.coordinatorLayout + private lateinit var sharedActionViewModel: SpacePeopleSharedActionViewModel override fun initUiAndData() { diff --git a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt index 1011952528..f8b5fada1a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/people/SpacePeopleListController.kt @@ -54,7 +54,7 @@ class SpacePeopleListController @Inject constructor( memberSummaries.forEach { memberEntry -> val filtered = memberEntry.second - .filter { roomMemberSummaryFilter.test(it) } + .filter { roomMemberSummaryFilter.test(it.summary) } if (filtered.isNotEmpty()) { dividerItem { id("divider_type_${memberEntry.first.titleRes}") @@ -65,10 +65,10 @@ class SpacePeopleListController @Inject constructor( .join( each = { _, roomMember -> profileMatrixItemWithPowerLevel { - id(roomMember.userId) - matrixItem(roomMember.toMatrixItem()) + id(roomMember.summary.userId) + matrixItem(roomMember.summary.toMatrixItem()) avatarRenderer(host.avatarRenderer) - userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.userId)) + userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.summary.userId)) .apply { val pl = host.toPowerLevelLabel(memberEntry.first) if (memberEntry.first == RoomMemberListCategories.INVITE) { @@ -106,13 +106,13 @@ class SpacePeopleListController @Inject constructor( } clickListener { - host.listener?.onSpaceMemberClicked(roomMember) + host.listener?.onSpaceMemberClicked(roomMember.summary) } } }, between = { _, roomMemberBefore -> dividerItem { - id("divider_${roomMemberBefore.userId}") + id("divider_${roomMemberBefore.summary.userId}") } } ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewModel.kt index 09b23bbed8..88c3348ed7 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/share/ShareSpaceViewModel.kt @@ -16,14 +16,13 @@ import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoomSummary -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.flow.flow class ShareSpaceViewModel @AssistedInject constructor( @Assisted private val initialState: ShareSpaceViewState, @@ -50,13 +49,11 @@ class ShareSpaceViewModel @AssistedInject constructor( private fun observePowerLevel() { val room = session.getRoom(initialState.spaceId) ?: return - PowerLevelsFlowFactory(room) - .createFlow() - .onEach { powerLevelContent -> - val powerLevelsHelper = PowerLevelsHelper(powerLevelContent) + room.flow().liveRoomPowerLevels() + .onEach { roomPowerLevels -> setState { copy( - canInviteByMxId = powerLevelsHelper.isUserAbleToInvite(session.myUserId) + canInviteByMxId = roomPowerLevels.isUserAbleToInvite(session.myUserId) ) } } diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt index 06fc143e21..7b27ffdd1e 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeActivity.kt @@ -11,6 +11,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable +import android.view.View import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.view.isVisible @@ -51,6 +52,9 @@ class UserCodeActivity : VectorBaseActivity(), override fun getCoordinatorLayout() = views.coordinatorLayout + override val rootView: View + get() = views.coordinatorLayout + private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { if (f is MatrixToBottomSheet) { diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt index f53571e753..b905ff4447 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderL.kt @@ -7,6 +7,7 @@ package im.vector.app.features.voice +import android.Manifest import android.content.Context import android.media.AudioFormat import android.media.AudioRecord @@ -15,6 +16,7 @@ import android.media.audiofx.AutomaticGainControl import android.media.audiofx.NoiseSuppressor import android.os.Build import android.widget.Toast +import im.vector.app.core.utils.PermissionChecker import io.element.android.opusencoder.OggOpusEncoder import io.element.android.opusencoder.configuration.SampleRate import kotlinx.coroutines.CoroutineScope @@ -22,6 +24,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull +import timber.log.Timber import kotlin.coroutines.CoroutineContext /** @@ -31,6 +34,7 @@ class VoiceRecorderL( private val context: Context, coroutineContext: CoroutineContext, private val codec: OggOpusEncoder, + private val permissionChecker: PermissionChecker, ) : AbstractVoiceRecorder(context) { companion object { @@ -127,7 +131,11 @@ class VoiceRecorderL( bufferSizeInShorts = AudioRecord.getMinBufferSize(SAMPLE_RATE.value, channelConfig, format) // Buffer is created as a ShortArray, but AudioRecord needs the size in bytes val bufferSizeInBytes = bufferSizeInShorts * 2 - audioRecorder = AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE.value, channelConfig, format, bufferSizeInBytes) + if (permissionChecker.checkPermission(Manifest.permission.RECORD_AUDIO)) { + audioRecorder = AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE.value, channelConfig, format, bufferSizeInBytes) + } else { + Timber.w("Not allowed to record audio.") + } } private fun calculateMaxAmplitude(buffer: ShortArray) { diff --git a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt index 0b971907a2..62309aa59b 100644 --- a/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt +++ b/vector/src/main/java/im/vector/app/features/voice/VoiceRecorderProvider.kt @@ -13,6 +13,7 @@ import android.media.MediaFormat import android.os.Build import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.VisibleForTesting +import im.vector.app.core.utils.PermissionChecker import im.vector.app.features.VectorFeatures import io.element.android.opusencoder.OggOpusEncoder import kotlinx.coroutines.Dispatchers @@ -23,12 +24,13 @@ class VoiceRecorderProvider @Inject constructor( private val context: Context, private val vectorFeatures: VectorFeatures, private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, + private val permissionChecker: PermissionChecker, ) { fun provideVoiceRecorder(): VoiceRecorder { return if (useNativeRecorder()) { VoiceRecorderQ(context) } else { - VoiceRecorderL(context, Dispatchers.IO, OggOpusEncoder.create()) + VoiceRecorderL(context, Dispatchers.IO, OggOpusEncoder.create(), permissionChecker) } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 233cb82d5d..3add1751b1 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -25,18 +25,13 @@ import im.vector.lib.multipicker.utils.toMultiPickerAudioType import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.jetbrains.annotations.VisibleForTesting -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.Room -import org.matrix.android.sdk.api.session.room.getStateEvent -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.getRoomPowerLevels import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.unwrap import timber.log.Timber @@ -139,12 +134,8 @@ class StartVoiceBroadcastUseCase @Inject constructor( @VisibleForTesting fun assertHasEnoughPowerLevels(room: Room) { - val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - ?.toModel() - ?.let { PowerLevelsHelper(it) } - - if (powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true) { + val roomPowerLevels = room.getRoomPowerLevels() + if (!roomPowerLevels.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO)) { Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission") throw VoiceBroadcastFailure.RecordingError.NoPermission } diff --git a/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt b/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt index 7af0dc62dc..6e7cb9e468 100644 --- a/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/webview/VectorWebViewActivity.kt @@ -9,6 +9,7 @@ package im.vector.app.features.webview import android.content.Context import android.content.Intent +import android.view.View import android.webkit.WebChromeClient import android.webkit.WebView import dagger.hilt.android.AndroidEntryPoint @@ -28,6 +29,9 @@ class VectorWebViewActivity : VectorBaseActivity() override fun getBinding() = ActivityVectorWebViewBinding.inflate(layoutInflater) + override val rootView: View + get() = views.mainRoot + val session: Session by lazy { activeSessionHolder.getActiveSession() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 099f39f67a..917fc02440 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -20,6 +20,7 @@ import android.content.IntentFilter import android.graphics.drawable.Icon import android.os.Build import android.util.Rational +import android.view.View import androidx.annotation.RequiresApi import androidx.core.app.PictureInPictureModeChangedInfo import androidx.core.content.ContextCompat @@ -77,6 +78,9 @@ class WidgetActivity : VectorBaseActivity() { override fun getBinding() = ActivityWidgetBinding.inflate(layoutInflater) + override val rootView: View + get() = views.mainRoot + override fun getTitleRes() = CommonStrings.room_widget_activity_title override fun initUiAndData() { diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt index 61f79662b0..40c64c7f19 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetPostAPIHandler.kt @@ -23,12 +23,10 @@ import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.getRoomPowerLevels import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.model.Membership -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator import org.matrix.android.sdk.api.util.JsonDict import timber.log.Timber @@ -146,13 +144,8 @@ class WidgetPostAPIHandler @AssistedInject constructor( Timber.d("## canSendEvent() : eventType $eventType isState $isState") - val powerLevelsEvent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - val powerLevelsContent = powerLevelsEvent?.content?.toModel() - val canSend = if (powerLevelsContent == null) { - false - } else { - PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(session.myUserId, isState, eventType) - } + val roomPowerLevels = room.getRoomPowerLevels() + val canSend = roomPowerLevels.isUserAllowedToSend(session.myUserId, isState, eventType) if (canSend) { Timber.d("## canSendEvent() returns true") widgetPostAPIMediator.sendBoolResponse(true, eventData) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index 6580b52fca..1c4c19b4f5 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -26,16 +26,10 @@ import kotlinx.coroutines.launch import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService -import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent -import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.flow.mapOptional -import org.matrix.android.sdk.flow.unwrap import timber.log.Timber import javax.net.ssl.HttpsURLConnection @@ -102,11 +96,9 @@ class WidgetViewModel @AssistedInject constructor( if (room == null) { return } - room.flow().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - .mapOptional { it.content.toModel() } - .unwrap() - .map { - PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, null) + room.flow().liveRoomPowerLevels() + .map { roomPowerLevels -> + roomPowerLevels.isUserAllowedToSend(session.myUserId, true, null) } .setOnEach { copy(canManageWidgets = it) diff --git a/vector/src/main/res/layout/activity_big_image_viewer.xml b/vector/src/main/res/layout/activity_big_image_viewer.xml index 8f1cf88aff..cd254db50d 100644 --- a/vector/src/main/res/layout/activity_big_image_viewer.xml +++ b/vector/src/main/res/layout/activity_big_image_viewer.xml @@ -1,6 +1,7 @@ @@ -28,4 +29,4 @@ app:layout_constraintTop_toBottomOf="@id/appBarLayout" app:optimizeDisplay="true" /> - \ No newline at end of file + diff --git a/vector/src/main/res/layout/activity_bug_report.xml b/vector/src/main/res/layout/activity_bug_report.xml index 1c019c858d..799ab2fb5f 100644 --- a/vector/src/main/res/layout/activity_bug_report.xml +++ b/vector/src/main/res/layout/activity_bug_report.xml @@ -2,6 +2,7 @@ diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index 5734e5f92a..02c90f8e54 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -97,7 +97,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/transparent" - android:fitsSystemWindows="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/vector/src/main/res/layout/activity_location_sharing.xml b/vector/src/main/res/layout/activity_location_sharing.xml index bbb46de8c7..9e44c72c83 100755 --- a/vector/src/main/res/layout/activity_location_sharing.xml +++ b/vector/src/main/res/layout/activity_location_sharing.xml @@ -1,5 +1,6 @@ @@ -21,4 +22,4 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> - \ No newline at end of file + diff --git a/vector/src/main/res/layout/activity_main.xml b/vector/src/main/res/layout/activity_main.xml index ba5925f000..626eb100d4 100644 --- a/vector/src/main/res/layout/activity_main.xml +++ b/vector/src/main/res/layout/activity_main.xml @@ -3,6 +3,7 @@ diff --git a/vector/src/main/res/layout/activity_widget.xml b/vector/src/main/res/layout/activity_widget.xml index b278bb5a1a..f1222d11df 100755 --- a/vector/src/main/res/layout/activity_widget.xml +++ b/vector/src/main/res/layout/activity_widget.xml @@ -1,6 +1,7 @@ @@ -23,4 +24,4 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> - \ No newline at end of file + diff --git a/vector/src/main/res/layout/dialog_edit_power_level.xml b/vector/src/main/res/layout/dialog_edit_power_level.xml index bf91dc3367..00f6fbce56 100644 --- a/vector/src/main/res/layout/dialog_edit_power_level.xml +++ b/vector/src/main/res/layout/dialog_edit_power_level.xml @@ -1,5 +1,7 @@ - - + android:text="@string/power_level_owner" + android:textColor="?vctr_content_primary" /> - - - - - - - - - - - + android:text="@string/power_level_admin" + android:textColor="?vctr_content_primary" /> - + - + - + diff --git a/vector/src/main/res/layout/fragment_generic_recycler.xml b/vector/src/main/res/layout/fragment_generic_recycler.xml index 472f05092e..1c2eb2fc40 100644 --- a/vector/src/main/res/layout/fragment_generic_recycler.xml +++ b/vector/src/main/res/layout/fragment_generic_recycler.xml @@ -2,6 +2,7 @@ diff --git a/vector/src/main/res/layout/fragment_new_home_detail.xml b/vector/src/main/res/layout/fragment_new_home_detail.xml index d20223a382..32d1dfc5d2 100644 --- a/vector/src/main/res/layout/fragment_new_home_detail.xml +++ b/vector/src/main/res/layout/fragment_new_home_detail.xml @@ -45,7 +45,6 @@ android:id="@+id/appBarLayout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:fitsSystemWindows="true" app:layout_constraintTop_toBottomOf="@id/syncStateView"> () private val fakeLocationTracker = FakeLocationTracker() + private val fakePermissionChecker = FakePermissionChecker() private fun createViewModel(): LiveLocationMapViewModel { return LiveLocationMapViewModel( @@ -47,6 +49,7 @@ class LiveLocationMapViewModelTest { locationSharingServiceConnection = fakeLocationSharingServiceConnection.instance, stopLiveLocationShareUseCase = fakeStopLiveLocationShareUseCase, locationTracker = fakeLocationTracker.instance, + permissionChecker = fakePermissionChecker ) } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePermissionChecker.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePermissionChecker.kt new file mode 100644 index 0000000000..5665a706a7 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePermissionChecker.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2022-2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package im.vector.app.test.fakes + +import im.vector.app.core.utils.PermissionChecker + +class FakePermissionChecker(val permissionResult: Boolean = true) : PermissionChecker { + override fun checkPermission(vararg permissions: String): Boolean { + return permissionResult + } +}