Merge branch 'develop' into feature/bca/rust_flavor

This commit is contained in:
valere 2022-12-03 11:15:46 +01:00
commit 03379a6636
47 changed files with 428 additions and 210 deletions

View File

@ -10,7 +10,6 @@ body:
id: checklist
attributes:
label: Release checklist
description: For the template example, we are releasing the version 1.2.3. Replace 1.2.3 with the version in the issue body.
placeholder: |
If you are reading this, you have deleted the content of the release template: undo the deletion or start again.
value: |

View File

@ -11,7 +11,7 @@ jobs:
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
uses: danger/danger-js@11.1.4
uses: danger/danger-js@11.2.0
with:
args: "--dangerfile tools/danger/dangerfile.js"
env:

View File

@ -66,7 +66,7 @@ jobs:
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
uses: danger/danger-js@11.1.4
uses: danger/danger-js@11.2.0
with:
args: "--dangerfile tools/danger/dangerfile-lint.js"
env:

View File

@ -17,7 +17,8 @@ jobs:
contains(github.event.issue.labels.*.name, 'Z-IA') ||
contains(github.event.issue.labels.*.name, 'A-Themes-Custom') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
contains(github.event.issue.labels.*.name, 'A-Tags')
contains(github.event.issue.labels.*.name, 'A-Tags') ||
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor')
steps:
- uses: actions/github-script@v5
with:

View File

@ -1,3 +1,39 @@
Changes in Element v1.5.10 (2022-11-30)
=======================================
Features ✨
----------
- Add setting to allow disabling direct share ([#2725](https://github.com/vector-im/element-android/issues/2725))
- [Device Manager] Toggle IP address visibility ([#7546](https://github.com/vector-im/element-android/issues/7546))
- New implementation of the full screen mode for the Rich Text Editor. ([#7577](https://github.com/vector-im/element-android/issues/7577))
Bugfixes 🐛
----------
- Fix italic text is truncated when bubble mode and markdown is enabled ([#5679](https://github.com/vector-im/element-android/issues/5679))
- Missing translations on "replyTo" messages ([#7555](https://github.com/vector-im/element-android/issues/7555))
- ANR on session start when sending client info is enabled ([#7604](https://github.com/vector-im/element-android/issues/7604))
- Make the plain text mode layout of the RTE more compact. ([#7620](https://github.com/vector-im/element-android/issues/7620))
- Push notification for thread message is now shown correctly when user observes rooms main timeline ([#7634](https://github.com/vector-im/element-android/issues/7634))
- Voice Broadcast - Fix playback stuck in buffering mode ([#7646](https://github.com/vector-im/element-android/issues/7646))
In development 🚧
----------------
- Voice Broadcast - Handle redaction of the state events on the listener and recorder sides ([#7629](https://github.com/vector-im/element-android/issues/7629))
- Voice Broadcast - Update the buffering display in the timeline ([#7655](https://github.com/vector-im/element-android/issues/7655))
- Voice Broadcast - Remove voice messages related to a VB from the room attachments ([#7656](https://github.com/vector-im/element-android/issues/7656))
SDK API changes ⚠️
------------------
- Added support for read receipts in threads. Now user in a room can have multiple read receipts (one per thread + one in main thread + one without threadId) ([#6996](https://github.com/vector-im/element-android/issues/6996))
- Sync Filter now taking in account homeserver capabilities to not pass unsupported parameters.
Sync Filter is now configured by providing SyncFilterBuilder class instance, instead of Filter to identify Filter changes related to homeserver capabilities ([#7626](https://github.com/vector-im/element-android/issues/7626))
Other changes
-------------
- Remove usage of Buildkite. ([#7583](https://github.com/vector-im/element-android/issues/7583))
- Better validation of edits ([#7594](https://github.com/vector-im/element-android/issues/7594))
Changes in Element v1.5.8 (2022-11-17)
======================================

View File

@ -1 +0,0 @@
Add setting to allow disabling direct share

View File

@ -1 +0,0 @@
Fix italic text is truncated when bubble mode and markdown is enabled

View File

@ -1 +0,0 @@
Added support for read receipts in threads. Now user in a room can have multiple read receipts (one per thread + one in main thread + one without threadId)

1
changelog.d/7477.misc Normal file
View File

@ -0,0 +1 @@
Add Z-Labs label for rich text editor and migrate to new label naming.

View File

@ -1 +0,0 @@
[Device Manager] Toggle IP address visibility

View File

@ -1 +0,0 @@
Missing translations on "replyTo" messages

View File

@ -1 +0,0 @@
New implementation of the full screen mode for the Rich Text Editor.

View File

@ -1 +0,0 @@
Remove usage of Buildkite.

View File

@ -1 +0,0 @@
Better validation of edits

View File

@ -1 +0,0 @@
ANR on session start when sending client info is enabled

View File

@ -1 +0,0 @@
Make the plain text mode layout of the RTE more compact.

View File

@ -1,2 +0,0 @@
Sync Filter now taking in account homeserver capabilities to not pass unsupported parameters.
Sync Filter is now configured by providing SyncFilterBuilder class instance, instead of Filter to identify Filter changes related to homeserver capabilities

View File

@ -1 +0,0 @@
Voice Broadcast - Handle redaction of the state events on the listener and recorder sides

View File

@ -1 +0,0 @@
Push notification for thread message is now shown correctly when user observes rooms main timeline

View File

@ -1 +0,0 @@
Voice Broadcast - Fix playback stuck in buffering mode

View File

@ -1 +0,0 @@
Voice Broadcast - Update the buffering display in the timeline

View File

@ -1 +0,0 @@
Voice Broadcast - Remove voice messages related to a VB from the room attachments

1
changelog.d/7658.bugfix Normal file
View File

@ -0,0 +1 @@
[Rich text editor] Fix design and spacing of rich text editor

1
changelog.d/7659.bugfix Normal file
View File

@ -0,0 +1 @@
[Rich text editor] Fix keyboard closing after collapsing editor

3
changelog.d/7680.bugfix Normal file
View File

@ -0,0 +1,3 @@
Rich Text Editor: fix several issues related to insets:
* Empty space displayed at the bottom when you don't have permissions to send messages into a room.
* Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it.

2
changelog.d/7683.bugfix Normal file
View File

@ -0,0 +1,2 @@
Fix crash in message composer when room is missing

1
changelog.d/7684.bugfix Normal file
View File

@ -0,0 +1 @@
Fix crash when invalid homeserver url is entered.

View File

@ -17,7 +17,7 @@ def markwon = "4.6.2"
def moshi = "1.14.0"
def lifecycle = "2.5.1"
def flowBinding = "1.2.0"
def flipper = "0.174.0"
def flipper = "0.176.0"
def epoxy = "5.0.0"
def mavericks = "3.0.1"
def glide = "4.14.2"
@ -26,7 +26,7 @@ def jjwt = "0.11.5"
// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
// the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "0.16.0-SNAPSHOT"
def sentry = "6.7.0"
def sentry = "6.9.0"
def fragment = "1.5.4"
// Testing
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819

View File

@ -0,0 +1,2 @@
Main changes in this version: New implementation of the full screen mode for the Rich Text Editor and bugfixes.
Full changelog: https://github.com/vector-im/element-android/releases

Binary file not shown.

View File

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
distributionSha256Sum=312eb12875e1747e05c2f81a4789902d7e4ec5defbd1eefeaccc08acf096505d
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

12
gradlew vendored
View File

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,10 +80,10 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# 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"'
@ -143,12 +143,16 @@ fi
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
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac

1
gradlew.bat vendored
View File

@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

View File

@ -63,7 +63,7 @@ android {
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.5.10\""
buildConfigField "String", "SDK_VERSION", "\"1.5.12\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""

View File

@ -19,32 +19,81 @@
# Ignore any error to not stop the script
set +e
printf "\n"
printf "================================================================================\n"
printf "\n================================================================================\n"
printf "| Welcome to the release script! |\n"
printf "================================================================================\n"
releaseScriptLocation="${RELEASE_SCRIPT_PATH}"
printf "Checking environment...\n"
envError=0
if [[ -z "${releaseScriptLocation}" ]]; then
printf "Fatal: RELEASE_SCRIPT_PATH is not defined in the environment. Please set to the path of your local file 'releaseElement2.sh'.\n"
exit 1
# Path of the key store (it's a file)
keyStorePath="${ELEMENT_KEYSTORE_PATH}"
if [[ -z "${keyStorePath}" ]]; then
printf "Fatal: ELEMENT_KEYSTORE_PATH is not defined in the environment.\n"
envError=1
fi
# Keystore password
keyStorePassword="${ELEMENT_KEYSTORE_PASSWORD}"
if [[ -z "${keyStorePassword}" ]]; then
printf "Fatal: ELEMENT_KEYSTORE_PASSWORD is not defined in the environment.\n"
envError=1
fi
# Key password
keyPassword="${ELEMENT_KEY_PASSWORD}"
if [[ -z "${keyPassword}" ]]; then
printf "Fatal: ELEMENT_KEY_PASSWORD is not defined in the environment.\n"
envError=1
fi
# GitHub token
gitHubToken="${ELEMENT_GITHUB_TOKEN}"
if [[ -z "${gitHubToken}" ]]; then
printf "Fatal: ELEMENT_GITHUB_TOKEN is not defined in the environment.\n"
envError=1
fi
# Android home
androidHome="${ANDROID_HOME}"
if [[ -z "${androidHome}" ]]; then
printf "Fatal: ANDROID_HOME is not defined in the environment.\n"
envError=1
fi
# @elementbot:matrix.org matrix token / Not mandatory
elementBotToken="${ELEMENT_BOT_MATRIX_TOKEN}"
if [[ -z "${elementBotToken}" ]]; then
printf "Warning: ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment.\n"
fi
releaseScriptFullPath="${releaseScriptLocation}/releaseElement2.sh"
if [[ ! -f ${releaseScriptFullPath} ]]; then
printf "Fatal: release script not found at ${releaseScriptFullPath}.\n"
if [ ${envError} == 1 ]; then
exit 1
fi
buildToolsVersion="30.0.2"
buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}"
if [[ ! -d ${buildToolsPath} ]]; then
printf "Fatal: ${buildToolsPath} folder not found, ensure that you have installed the SDK version ${buildToolsVersion}.\n"
exit 1
fi
# Check if git flow is enabled
git flow config >/dev/null 2>&1
if [[ $? == 0 ]]
then
printf "Git flow is initialized\n"
else
printf "Git flow is not initialized. Initializing...\n"
# All default value, just set 'v' for tag prefix
git flow init -d -t 'v'
fi
printf "OK\n"
printf "\n================================================================================\n"
# Guessing version to propose a default version
versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut -d " " -f3`
versionMinorCandidate=`grep "ext.versionMinor" ./vector-app/build.gradle | cut -d " " -f3`
versionPatchCandidate=`grep "ext.versionPatch" ./vector-app/build.gradle | cut -d " " -f3`
versionCandidate="${versionMajorCandidate}.${versionMinorCandidate}.${versionPatchCandidate}"
printf "\n"
read -p "Please enter the release version (example: ${versionCandidate}). Just press enter if ${versionCandidate} is correct. " version
version=${version:-${versionCandidate}}
@ -77,7 +126,7 @@ fi
cp ./vector-app/build.gradle ./vector-app/build.gradle.bak
sed "s/ext.versionMajor = .*/ext.versionMajor = ${versionMajor}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle
sed "s/ext.versionMinor = .*/ext.versionMinor = ${versionMinor}/" ./vector-app/build.gradle > ./vector-app/build.gradle.bak
sed "s/ext.versionPatch = .*/ext.versionPatch = ${patchVersion}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle
sed "s/ext.versionPatch = .*/ext.versionPatch = ${versionPatch}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle
rm ./vector-app/build.gradle.bak
cp ./matrix-sdk-android/build.gradle ./matrix-sdk-android/build.gradle.bak
sed "s/\"SDK_VERSION\", .*$/\"SDK_VERSION\", \"\\\\\"${version}\\\\\"\"/" ./matrix-sdk-android/build.gradle.bak > ./matrix-sdk-android/build.gradle
@ -155,6 +204,7 @@ fastlanePathFile="./fastlane/metadata/android/en-US/changelogs/${fastlaneFile}"
printf "Main changes in this version: TODO.\nFull changelog: https://github.com/vector-im/element-android/releases" > ${fastlanePathFile}
read -p "I have created the file ${fastlanePathFile}, please edit it and press enter when it's done."
git add ${fastlanePathFile}
git commit -a -m "Adding fastlane file for version ${version}"
printf "\n================================================================================\n"
@ -184,7 +234,6 @@ git checkout develop
# Set next version
printf "\n================================================================================\n"
printf "Setting next version on file './vector-app/build.gradle'...\n"
nextPatchVersion=$((versionPatch + 2))
cp ./vector-app/build.gradle ./vector-app/build.gradle.bak
sed "s/ext.versionPatch = .*/ext.versionPatch = ${nextPatchVersion}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle
rm ./vector-app/build.gradle.bak
@ -214,17 +263,93 @@ else
fi
printf "\n================================================================================\n"
read -p "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch. Press enter when it's done."
printf "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch.\n"
read -p "After GHA is finished, please enter the artifact URL (for 'vector-gplay-release-unsigned'): " artifactUrl
printf "\n================================================================================\n"
printf "Running the release script...\n"
cd ${releaseScriptLocation}
${releaseScriptFullPath} "v${version}"
cd -
printf "Downloading the artifact...\n"
# Download files
targetPath="./tmp/Element/${version}"
# Ignore error
set +e
python3 ./tools/release/download_github_artifacts.py \
--token ${gitHubToken} \
--artifactUrl ${artifactUrl} \
--directory ${targetPath} \
--ignoreErrors
# Do not ignore error
set -e
printf "\n================================================================================\n"
apkPath="${releaseScriptLocation}/Element/v${version}/vector-gplay-arm64-v8a-release-signed.apk"
printf "Installing apk on a real device...\n"
printf "Unzipping the artifact...\n"
unzip ${targetPath}/vector-gplay-release-unsigned.zip -d ${targetPath}
# Flatten folder hierarchy
mv ${targetPath}/gplay/release/* ${targetPath}
rm -rf ${targetPath}/gplay
printf "\n================================================================================\n"
printf "Signing the APKs...\n"
cp ${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk \
${targetPath}/vector-gplay-arm64-v8a-release-signed.apk
./tools/release/sign_apk_unsafe.sh \
${keyStorePath} \
${targetPath}/vector-gplay-arm64-v8a-release-signed.apk \
${keyStorePassword} \
${keyPassword}
cp ${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk \
${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk
./tools/release/sign_apk_unsafe.sh \
${keyStorePath} \
${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk \
${keyStorePassword} \
${keyPassword}
cp ${targetPath}/vector-gplay-x86-release-unsigned.apk \
${targetPath}/vector-gplay-x86-release-signed.apk
./tools/release/sign_apk_unsafe.sh \
${keyStorePath} \
${targetPath}/vector-gplay-x86-release-signed.apk \
${keyStorePassword} \
${keyPassword}
cp ${targetPath}/vector-gplay-x86_64-release-unsigned.apk \
${targetPath}/vector-gplay-x86_64-release-signed.apk
./tools/release/sign_apk_unsafe.sh \
${keyStorePath} \
${targetPath}/vector-gplay-x86_64-release-signed.apk \
${keyStorePassword} \
${keyPassword}
# Ref: https://docs.fastlane.tools/getting-started/android/beta-deployment/#uploading-your-app
# set SUPPLY_APK_PATHS="${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk,${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk,${targetPath}/vector-gplay-x86-release-unsigned.apk,${targetPath}/vector-gplay-x86_64-release-unsigned.apk"
#
# ./fastlane beta
printf "\n================================================================================\n"
printf "Please check the information below:\n"
printf "File vector-gplay-arm64-v8a-release-signed.apk:\n"
${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk | grep package
printf "File vector-gplay-armeabi-v7a-release-signed.apk:\n"
${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk | grep package
printf "File vector-gplay-x86-release-signed.apk:\n"
${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86-release-signed.apk | grep package
printf "File vector-gplay-x86_64-release-signed.apk:\n"
${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86_64-release-signed.apk | grep package
read -p "\nDoes it look correct? Press enter when it's done."
printf "\n================================================================================\n"
read -p "Installing apk on a real device, press enter when a real device is connected. "
apkPath="${targetPath}/vector-gplay-arm64-v8a-release-signed.apk"
adb -d install ${apkPath}
read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done."
@ -234,9 +359,25 @@ read -p "Create the release on gitHub from the tag https://github.com/vector-im/
read -p "Add the 4 signed APKs to the GitHub release. Press enter when it's done."
printf "\n================================================================================\n"
printf "Ping the Android Internal room. Here is an example of message which can be sent:\n\n"
printf "@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!\n\n"
read -p "Press enter when it's done."
printf "Message for the Android internal room:\n\n"
message="@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!"
printf "${message}\n\n"
if [[ -z "${elementBotToken}" ]]; then
read -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done "
else
read -p "Send this message to the room (yes/no) default to yes? " doSend
doSend=${doSend:-yes}
if [ ${doSend} == "yes" ]; then
printf "Sending message...\n"
transactionId=`openssl rand -hex 16`
# Element Android internal
matrixRoomId="!LiSLXinTDCsepePiYW:matrix.org"
curl -X PUT --data $"{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${elementBotToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local.${transactionId}
else
printf "Message not sent, please send it manually!\n"
fi
fi
printf "\n================================================================================\n"
printf "Congratulation! Kudos for using this script! Have a nice day!\n"

View File

@ -37,7 +37,7 @@ ext.versionMinor = 5
// 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 = 10
ext.versionPatch = 12
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@ -440,13 +440,13 @@ dependencies {
// Plant Timber tree for test
androidTestImplementation libs.tests.timberJunitRule
// "The one who serves a great Espresso"
androidTestImplementation('com.adevinta.android:barista:4.2.0') {
androidTestImplementation('com.adevinta.android:barista:4.3.0') {
exclude group: 'org.jetbrains.kotlin'
}
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
androidTestImplementation libs.androidx.fragmentTesting
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21"
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
debugImplementation libs.androidx.fragmentTesting
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
}

View File

@ -328,11 +328,11 @@ dependencies {
// Plant Timber tree for test
androidTestImplementation libs.tests.timberJunitRule
// "The one who serves a great Espresso"
androidTestImplementation('com.adevinta.android:barista:4.2.0') {
androidTestImplementation('com.adevinta.android:barista:4.3.0') {
exclude group: 'org.jetbrains.kotlin'
}
androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator
debugImplementation libs.androidx.fragmentTesting
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21"
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
}

View File

@ -608,26 +608,33 @@ class ExpandingBottomSheetBehavior<V : View> : CoordinatorLayout.Behavior<V> {
initialPaddingBottom = view.paddingBottom
// This should only be used to set initial insets and other edge cases where the insets can't be applied using an animation.
var applyInsetsFromAnimation = false
var isAnimating = false
// This will animated inset changes, making them look a lot better. However, it won't update initial insets.
// This will animate inset changes, making them look a lot better. However, it won't update initial insets.
ViewCompat.setWindowInsetsAnimationCallback(view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
isAnimating = true
}
override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
return applyInsets(view, insets)
return if (isAnimating) {
applyInsets(view, insets)
} else {
insets
}
}
override fun onEnd(animation: WindowInsetsAnimationCompat) {
applyInsetsFromAnimation = false
isAnimating = false
view.requestApplyInsets()
}
})
ViewCompat.setOnApplyWindowInsetsListener(view) { _: View, insets: WindowInsetsCompat ->
if (!applyInsetsFromAnimation) {
applyInsetsFromAnimation = true
applyInsets(view, insets)
} else {
if (isAnimating) {
insets
} else {
applyInsets(view, insets)
}
}

View File

@ -255,7 +255,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
) { mainState, messageComposerState, attachmentState ->
if (mainState.tombstoneEvent != null) return@withState
(composer as? View)?.isInvisible = !messageComposerState.isComposerVisible
(composer as? View)?.isVisible = messageComposerState.isComposerVisible
composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
(composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled
}

View File

@ -59,6 +59,7 @@ 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.getRoomSummary
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.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
@ -89,39 +90,44 @@ class MessageComposerViewModel @AssistedInject constructor(
private val voiceBroadcastHelper: VoiceBroadcastHelper,
) : VectorViewModel<MessageComposerViewState, MessageComposerAction, MessageComposerViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId)!!
private val room = session.getRoom(initialState.roomId)
// Keep it out of state to avoid invalidate being called
private var currentComposerText: CharSequence = ""
init {
loadDraftIfAny()
observePowerLevelAndEncryption()
observeVoiceBroadcast()
subscribeToStateInternal()
if (room != null) {
loadDraftIfAny(room)
observePowerLevelAndEncryption(room)
observeVoiceBroadcast(room)
subscribeToStateInternal()
} else {
onRoomError()
}
}
override fun handle(action: MessageComposerAction) {
val room = this.room ?: return
when (action) {
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action)
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action)
is MessageComposerAction.EnterEditMode -> handleEnterEditMode(room, action)
is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(room, action)
is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action)
is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action)
is MessageComposerAction.SendMessage -> handleSendMessage(action)
is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action)
is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(room, action)
is MessageComposerAction.SendMessage -> handleSendMessage(room, action)
is MessageComposerAction.UserIsTyping -> handleUserIsTyping(room, action)
is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action)
is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action)
is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage()
is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled, action.rootThreadEventId)
is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage(room)
is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(room, action.isCancelled, action.rootThreadEventId)
is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action)
MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage()
MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback()
is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData)
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText)
is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(room, action.attachmentData)
is MessageComposerAction.OnEntersBackground -> handleEntersBackground(room, action.composerText)
is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action)
is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action)
is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action)
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action)
is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(room, action)
is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action)
is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action)
}
@ -157,7 +163,7 @@ class MessageComposerViewModel @AssistedInject constructor(
copy(sendMode = SendMode.Regular(currentComposerText, action.fromSharing))
}
private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) {
private fun handleEnterEditMode(room: Room, action: MessageComposerAction.EnterEditMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
val formatted = vectorPreferences.isRichTextEditorEnabled()
setState { copy(sendMode = SendMode.Edit(timelineEvent, timelineEvent.getTextEditableContent(formatted))) }
@ -168,7 +174,7 @@ class MessageComposerViewModel @AssistedInject constructor(
setState { copy(isFullScreen = action.isFullScreen) }
}
private fun observePowerLevelAndEncryption() {
private fun observePowerLevelAndEncryption(room: Room) {
combine(
PowerLevelsFlowFactory(room).createFlow(),
room.flow().liveRoomSummary().unwrap()
@ -194,7 +200,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun observeVoiceBroadcast() {
private fun observeVoiceBroadcast(room: Room) {
room.stateService().getStateEventLive(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(session.myUserId))
.asFlow()
.unwrap()
@ -204,19 +210,19 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) {
private fun handleEnterQuoteMode(room: Room, action: MessageComposerAction.EnterQuoteMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Quote(timelineEvent, currentComposerText)) }
}
}
private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) {
private fun handleEnterReplyMode(room: Room, action: MessageComposerAction.EnterReplyMode) {
room.getTimelineEvent(action.eventId)?.let { timelineEvent ->
setState { copy(sendMode = SendMode.Reply(timelineEvent, currentComposerText)) }
}
}
private fun handleSendMessage(action: MessageComposerAction.SendMessage) {
private fun handleSendMessage(room: Room, action: MessageComposerAction.SendMessage) {
withState { state ->
analyticsTracker.capture(state.toAnalyticsComposer()).also {
setState { copy(startsThread = false) }
@ -246,7 +252,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is ParsedCommand.ErrorSyntax -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandError(parsedCommand.command))
@ -272,7 +278,7 @@ class MessageComposerViewModel @AssistedInject constructor(
room.sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false)
}
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is ParsedCommand.SendFormattedText -> {
// Send the text message to the room, without markdown
@ -290,23 +296,23 @@ class MessageComposerViewModel @AssistedInject constructor(
)
}
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is ParsedCommand.ChangeRoomName -> {
handleChangeRoomNameSlashCommand(parsedCommand)
handleChangeRoomNameSlashCommand(room, parsedCommand)
}
is ParsedCommand.Invite -> {
handleInviteSlashCommand(parsedCommand)
handleInviteSlashCommand(room, parsedCommand)
}
is ParsedCommand.Invite3Pid -> {
handleInvite3pidSlashCommand(parsedCommand)
handleInvite3pidSlashCommand(room, parsedCommand)
}
is ParsedCommand.SetUserPowerLevel -> {
handleSetUserPowerLevel(parsedCommand)
handleSetUserPowerLevel(room, parsedCommand)
}
is ParsedCommand.DevTools -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.ClearScalarToken -> {
// TODO
@ -315,29 +321,29 @@ class MessageComposerViewModel @AssistedInject constructor(
is ParsedCommand.SetMarkdown -> {
vectorPreferences.setMarkdownEnabled(parsedCommand.enable)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.BanUser -> {
handleBanSlashCommand(parsedCommand)
handleBanSlashCommand(room, parsedCommand)
}
is ParsedCommand.UnbanUser -> {
handleUnbanSlashCommand(parsedCommand)
handleUnbanSlashCommand(room, parsedCommand)
}
is ParsedCommand.IgnoreUser -> {
handleIgnoreSlashCommand(parsedCommand)
handleIgnoreSlashCommand(room, parsedCommand)
}
is ParsedCommand.UnignoreUser -> {
handleUnignoreSlashCommand(parsedCommand)
}
is ParsedCommand.RemoveUser -> {
handleRemoveSlashCommand(parsedCommand)
handleRemoveSlashCommand(room, parsedCommand)
}
is ParsedCommand.JoinRoom -> {
handleJoinToAnotherRoomSlashCommand(parsedCommand)
popDraft()
popDraft(room)
}
is ParsedCommand.PartRoom -> {
handlePartSlashCommand(parsedCommand)
handlePartSlashCommand(room, parsedCommand)
}
is ParsedCommand.SendEmote -> {
if (state.rootThreadEventId != null) {
@ -355,7 +361,7 @@ class MessageComposerViewModel @AssistedInject constructor(
)
}
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendRainbow -> {
val message = parsedCommand.message.toString()
@ -369,7 +375,7 @@ class MessageComposerViewModel @AssistedInject constructor(
room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message))
}
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendRainbowEmote -> {
val message = parsedCommand.message.toString()
@ -385,7 +391,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendSpoiler -> {
val text = "[${stringProvider.getString(R.string.spoiler)}](${parsedCommand.message})"
@ -403,53 +409,53 @@ class MessageComposerViewModel @AssistedInject constructor(
)
}
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendShrug -> {
sendPrefixedMessage("¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId)
sendPrefixedMessage(room, "¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendLenny -> {
sendPrefixedMessage("( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId)
sendPrefixedMessage(room, "( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendTableFlip -> {
sendPrefixedMessage("(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId)
sendPrefixedMessage(room, "(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.SendChatEffect -> {
sendChatEffect(parsedCommand)
sendChatEffect(room, parsedCommand)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(parsedCommand)
handleChangeTopicSlashCommand(room, parsedCommand)
}
is ParsedCommand.ChangeDisplayName -> {
handleChangeDisplayNameSlashCommand(parsedCommand)
handleChangeDisplayNameSlashCommand(room, parsedCommand)
}
is ParsedCommand.ChangeDisplayNameForRoom -> {
handleChangeDisplayNameForRoomSlashCommand(parsedCommand)
handleChangeDisplayNameForRoomSlashCommand(room, parsedCommand)
}
is ParsedCommand.ChangeRoomAvatar -> {
handleChangeRoomAvatarSlashCommand(parsedCommand)
handleChangeRoomAvatarSlashCommand(room, parsedCommand)
}
is ParsedCommand.ChangeAvatarForRoom -> {
handleChangeAvatarForRoomSlashCommand(parsedCommand)
handleChangeAvatarForRoomSlashCommand(room, parsedCommand)
}
is ParsedCommand.ShowUser -> {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
handleWhoisSlashCommand(parsedCommand)
popDraft()
popDraft(room)
}
is ParsedCommand.DiscardSession -> {
if (room.roomCryptoService().isEncrypted()) {
session.cryptoService().discardOutboundSession(room.roomId)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
} else {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
_viewEvents.post(
@ -474,7 +480,7 @@ class MessageComposerViewModel @AssistedInject constructor(
null,
true
)
popDraft()
popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -493,7 +499,7 @@ class MessageComposerViewModel @AssistedInject constructor(
null,
false
)
popDraft()
popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -506,7 +512,7 @@ class MessageComposerViewModel @AssistedInject constructor(
viewModelScope.launch(Dispatchers.IO) {
try {
session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias)
popDraft()
popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -518,7 +524,7 @@ class MessageComposerViewModel @AssistedInject constructor(
viewModelScope.launch(Dispatchers.IO) {
try {
session.roomService().leaveRoom(parsedCommand.roomId)
popDraft()
popDraft(room)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
} catch (failure: Throwable) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure))
@ -534,7 +540,7 @@ class MessageComposerViewModel @AssistedInject constructor(
)
)
_viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand))
popDraft()
popDraft(room)
}
}
}
@ -583,7 +589,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is SendMode.Quote -> {
room.sendService().sendQuotedTextMessage(
@ -594,7 +600,7 @@ class MessageComposerViewModel @AssistedInject constructor(
rootThreadEventId = state.rootThreadEventId
)
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is SendMode.Reply -> {
val timelineEvent = state.sendMode.timelineEvent
@ -619,7 +625,7 @@ class MessageComposerViewModel @AssistedInject constructor(
)
_viewEvents.post(MessageComposerViewEvents.MessageSent)
popDraft()
popDraft(room)
}
is SendMode.Voice -> {
// do nothing
@ -628,10 +634,10 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun popDraft() = withState {
private fun popDraft(room: Room) = withState {
if (it.sendMode is SendMode.Regular && it.sendMode.fromSharing) {
// If we were sharing, we want to get back our last value from draft
loadDraftIfAny()
loadDraftIfAny(room)
} else {
// Otherwise we clear the composer and remove the draft from db
setState { copy(sendMode = SendMode.Regular("", false)) }
@ -641,7 +647,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun loadDraftIfAny() {
private fun loadDraftIfAny(room: Room) {
val currentDraft = room.draftService().getDraft()
setState {
copy(
@ -670,7 +676,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) {
private fun handleUserIsTyping(room: Room, action: MessageComposerAction.UserIsTyping) {
if (vectorPreferences.sendTypingNotifs()) {
if (action.isTyping) {
room.typingService().userIsTyping()
@ -680,7 +686,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) {
private fun sendChatEffect(room: Room, sendChatEffect: ParsedCommand.SendChatEffect) {
// If message is blank, convert to an emote, with default message
if (sendChatEffect.message.isBlank()) {
val defaultMessage = stringProvider.getString(
@ -732,25 +738,25 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
launchSlashCommandFlowSuspendable(changeTopic) {
private fun handleChangeTopicSlashCommand(room: Room, changeTopic: ParsedCommand.ChangeTopic) {
launchSlashCommandFlowSuspendable(room, changeTopic) {
room.stateService().updateTopic(changeTopic.topic)
}
}
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
launchSlashCommandFlowSuspendable(invite) {
private fun handleInviteSlashCommand(room: Room, invite: ParsedCommand.Invite) {
launchSlashCommandFlowSuspendable(room, invite) {
room.membershipService().invite(invite.userId, invite.reason)
}
}
private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) {
launchSlashCommandFlowSuspendable(invite) {
private fun handleInvite3pidSlashCommand(room: Room, invite: ParsedCommand.Invite3Pid) {
launchSlashCommandFlowSuspendable(room, invite) {
room.membershipService().invite3pid(invite.threePid)
}
}
private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
private fun handleSetUserPowerLevel(room: Room, setUserPowerLevel: ParsedCommand.SetUserPowerLevel) {
val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content
?.toModel<PowerLevelsContent>()
@ -758,19 +764,19 @@ class MessageComposerViewModel @AssistedInject constructor(
?.toContent()
?: return
launchSlashCommandFlowSuspendable(setUserPowerLevel) {
launchSlashCommandFlowSuspendable(room, setUserPowerLevel) {
room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent)
}
}
private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) {
launchSlashCommandFlowSuspendable(changeDisplayName) {
private fun handleChangeDisplayNameSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayName) {
launchSlashCommandFlowSuspendable(room, changeDisplayName) {
session.profileService().setDisplayName(session.myUserId, changeDisplayName.displayName)
}
}
private fun handlePartSlashCommand(command: ParsedCommand.PartRoom) {
launchSlashCommandFlowSuspendable(command) {
private fun handlePartSlashCommand(room: Room, command: ParsedCommand.PartRoom) {
launchSlashCommandFlowSuspendable(room, command) {
if (command.roomAlias == null) {
// Leave the current room
room
@ -785,39 +791,39 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) {
launchSlashCommandFlowSuspendable(removeUser) {
private fun handleRemoveSlashCommand(room: Room, removeUser: ParsedCommand.RemoveUser) {
launchSlashCommandFlowSuspendable(room, removeUser) {
room.membershipService().remove(removeUser.userId, removeUser.reason)
}
}
private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) {
launchSlashCommandFlowSuspendable(ban) {
private fun handleBanSlashCommand(room: Room, ban: ParsedCommand.BanUser) {
launchSlashCommandFlowSuspendable(room, ban) {
room.membershipService().ban(ban.userId, ban.reason)
}
}
private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) {
launchSlashCommandFlowSuspendable(unban) {
private fun handleUnbanSlashCommand(room: Room, unban: ParsedCommand.UnbanUser) {
launchSlashCommandFlowSuspendable(room, unban) {
room.membershipService().unban(unban.userId, unban.reason)
}
}
private fun handleChangeRoomNameSlashCommand(changeRoomName: ParsedCommand.ChangeRoomName) {
launchSlashCommandFlowSuspendable(changeRoomName) {
private fun handleChangeRoomNameSlashCommand(room: Room, changeRoomName: ParsedCommand.ChangeRoomName) {
launchSlashCommandFlowSuspendable(room, changeRoomName) {
room.stateService().updateName(changeRoomName.name)
}
}
private fun getMyRoomMemberContent(): RoomMemberContent? {
private fun getMyRoomMemberContent(room: Room): RoomMemberContent? {
return room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId))
?.content
?.toModel<RoomMemberContent>()
}
private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) {
launchSlashCommandFlowSuspendable(changeDisplayName) {
getMyRoomMemberContent()
private fun handleChangeDisplayNameForRoomSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) {
launchSlashCommandFlowSuspendable(room, changeDisplayName) {
getMyRoomMemberContent(room)
?.copy(displayName = changeDisplayName.displayName)
?.toContent()
?.let {
@ -826,15 +832,15 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) {
launchSlashCommandFlowSuspendable(changeAvatar) {
private fun handleChangeRoomAvatarSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeRoomAvatar) {
launchSlashCommandFlowSuspendable(room, changeAvatar) {
room.stateService().sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent())
}
}
private fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) {
launchSlashCommandFlowSuspendable(changeAvatar) {
getMyRoomMemberContent()
private fun handleChangeAvatarForRoomSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeAvatarForRoom) {
launchSlashCommandFlowSuspendable(room, changeAvatar) {
getMyRoomMemberContent(room)
?.copy(avatarUrl = changeAvatar.url)
?.toContent()
?.let {
@ -843,8 +849,8 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleIgnoreSlashCommand(ignore: ParsedCommand.IgnoreUser) {
launchSlashCommandFlowSuspendable(ignore) {
private fun handleIgnoreSlashCommand(room: Room, ignore: ParsedCommand.IgnoreUser) {
launchSlashCommandFlowSuspendable(room, ignore) {
session.userService().ignoreUserIds(listOf(ignore.userId))
}
}
@ -853,15 +859,15 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.SlashCommandConfirmationRequest(unignore))
}
private fun handleSlashCommandConfirmed(action: MessageComposerAction.SlashCommandConfirmed) {
private fun handleSlashCommandConfirmed(room: Room, action: MessageComposerAction.SlashCommandConfirmed) {
when (action.parsedCommand) {
is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(action.parsedCommand)
is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(room, action.parsedCommand)
else -> TODO("Not handled yet")
}
}
private fun handleUnignoreSlashCommandConfirmed(unignore: ParsedCommand.UnignoreUser) {
launchSlashCommandFlowSuspendable(unignore) {
private fun handleUnignoreSlashCommandConfirmed(room: Room, unignore: ParsedCommand.UnignoreUser) {
launchSlashCommandFlowSuspendable(room, unignore) {
session.userService().unIgnoreUserIds(listOf(unignore.userId))
}
}
@ -870,7 +876,7 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId))
}
private fun sendPrefixedMessage(prefix: String, message: CharSequence, rootThreadEventId: String?) {
private fun sendPrefixedMessage(room: Room, prefix: String, message: CharSequence, rootThreadEventId: String?) {
val sequence = buildString {
append(prefix)
if (message.isNotEmpty()) {
@ -886,7 +892,7 @@ class MessageComposerViewModel @AssistedInject constructor(
/**
* Convert a send mode to a draft and save the draft.
*/
private fun handleSaveTextDraft(draft: String) = withState {
private fun handleSaveTextDraft(room: Room, draft: String) = withState {
session.coroutineScope.launch {
when {
it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> {
@ -909,7 +915,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleStartRecordingVoiceMessage() {
private fun handleStartRecordingVoiceMessage(room: Room) {
try {
audioMessageHelper.startRecording(room.roomId)
} catch (failure: Throwable) {
@ -917,7 +923,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) {
private fun handleEndRecordingVoiceMessage(room: Room, isCancelled: Boolean, rootThreadEventId: String? = null) {
audioMessageHelper.stopPlayback()
if (isCancelled) {
audioMessageHelper.deleteRecording()
@ -964,7 +970,7 @@ class MessageComposerViewModel @AssistedInject constructor(
audioMessageHelper.stopAllVoiceActions(deleteRecord)
}
private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) {
private fun handleInitializeVoiceRecorder(room: Room, attachmentData: ContentAttachmentData) {
audioMessageHelper.initializeRecorder(room.roomId, attachmentData)
setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) }
}
@ -985,7 +991,7 @@ class MessageComposerViewModel @AssistedInject constructor(
audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration)
}
private fun handleEntersBackground(composerText: String) {
private fun handleEntersBackground(room: Room, composerText: String) {
// Always stop all voice actions. It may be playing in timeline or active recording
val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false)
// TODO remove this when there will be a listening indicator outside of the timeline
@ -1001,7 +1007,7 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
} else {
handleSaveTextDraft(draft = composerText)
handleSaveTextDraft(room = room, draft = composerText)
}
}
@ -1009,12 +1015,12 @@ class MessageComposerViewModel @AssistedInject constructor(
_viewEvents.post(MessageComposerViewEvents.InsertUserDisplayName(action.userId))
}
private fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) {
private fun launchSlashCommandFlowSuspendable(room: Room, parsedCommand: ParsedCommand, block: suspend () -> Unit) {
_viewEvents.post(MessageComposerViewEvents.SlashCommandLoading)
viewModelScope.launch {
val event = try {
block()
popDraft()
popDraft(room)
MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)
} catch (failure: Throwable) {
MessageComposerViewEvents.SlashCommandResultError(failure)
@ -1023,6 +1029,10 @@ class MessageComposerViewModel @AssistedInject constructor(
}
}
private fun onRoomError() = setState {
copy(isRoomError = true)
}
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<MessageComposerViewModel, MessageComposerViewState> {
override fun create(initialState: MessageComposerViewState): MessageComposerViewModel

View File

@ -62,6 +62,7 @@ fun CanSendStatus.boolean(): Boolean {
data class MessageComposerViewState(
val roomId: String,
val isRoomError: Boolean = false,
val canSendMessage: CanSendStatus = CanSendStatus.Allowed,
val isSendButtonVisible: Boolean = false,
val rootThreadEventId: String? = null,
@ -88,8 +89,8 @@ data class MessageComposerViewState(
val isVoiceMessageIdle = !isVoiceRecording
val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording
val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible
val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording && !isRoomError
val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible && !isRoomError
constructor(args: TimelineArgs) : this(
roomId = args.roomId,

View File

@ -42,7 +42,6 @@ import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.google.android.material.shape.MaterialShapeDrawable
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setTextIfDifferent
import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.utils.DimensionConverter
@ -132,8 +131,6 @@ class RichTextComposerLayout @JvmOverloads constructor(
views.bottomSheetHandle.isVisible = isFullScreen
if (isFullScreen) {
editText.showKeyboard(true)
} else {
editText.hideKeyboard()
}
this.isFullScreen = isFullScreen
}
@ -274,8 +271,8 @@ class RichTextComposerLayout @JvmOverloads constructor(
connect(R.id.composerEditTextOuterBorder, ConstraintSet.START, R.id.composerLayoutContent, ConstraintSet.START, dpToPx(12))
connect(R.id.composerEditTextOuterBorder, ConstraintSet.END, R.id.composerLayoutContent, ConstraintSet.END, dpToPx(12))
} else {
connect(R.id.composerEditTextOuterBorder, ConstraintSet.TOP, R.id.composerLayoutContent, ConstraintSet.TOP, dpToPx(10))
connect(R.id.composerEditTextOuterBorder, ConstraintSet.BOTTOM, R.id.composerLayoutContent, ConstraintSet.BOTTOM, dpToPx(10))
connect(R.id.composerEditTextOuterBorder, ConstraintSet.TOP, R.id.composerLayoutContent, ConstraintSet.TOP, dpToPx(8))
connect(R.id.composerEditTextOuterBorder, ConstraintSet.BOTTOM, R.id.composerLayoutContent, ConstraintSet.BOTTOM, dpToPx(8))
connect(R.id.composerEditTextOuterBorder, ConstraintSet.START, R.id.attachmentButton, ConstraintSet.END, 0)
connect(R.id.composerEditTextOuterBorder, ConstraintSet.END, R.id.sendButton, ConstraintSet.START, 0)
}

View File

@ -118,7 +118,7 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
private fun checkQrCodeLoginCapability(homeServerUrl: String) {
private suspend fun checkQrCodeLoginCapability(config: HomeServerConnectionConfig) {
if (!vectorFeatures.isQrCodeLoginEnabled()) {
setState {
copy(
@ -133,16 +133,12 @@ class OnboardingViewModel @AssistedInject constructor(
)
}
} else {
viewModelScope.launch {
// check if selected server supports MSC3882 first
homeServerConnectionConfigFactory.create(homeServerUrl)?.let {
val canLoginWithQrCode = authenticationService.isQrLoginSupported(it)
setState {
copy(
canLoginWithQrCode = canLoginWithQrCode
)
}
}
// check if selected server supports MSC3882 first
val canLoginWithQrCode = authenticationService.isQrLoginSupported(config)
setState {
copy(
canLoginWithQrCode = canLoginWithQrCode
)
}
}
}
@ -710,7 +706,6 @@ class OnboardingViewModel @AssistedInject constructor(
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
} else {
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction)
checkQrCodeLoginCapability(homeServerConnectionConfig.homeServerUri.toString())
}
}
@ -769,6 +764,8 @@ class OnboardingViewModel @AssistedInject constructor(
_viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
}
checkQrCodeLoginCapability(config)
when (trigger) {
is OnboardingAction.HomeServerChange.SelectHomeServer -> {
onHomeServerSelected(config, serverTypeOverride, authResult)

View File

@ -32,26 +32,26 @@
<ImageButton
android:id="@+id/attachmentButton"
android:layout_width="56dp"
android:layout_height="60dp"
android:layout_margin="@dimen/composer_attachment_margin"
android:background="?android:attr/selectableItemBackground"
android:layout_width="60dp"
android:layout_height="56dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/option_send_files"
android:src="@drawable/ic_rich_composer_add"
android:paddingStart="4dp"
app:layout_constraintVertical_bias="1"
app:layout_constraintBottom_toBottomOf="@id/sendButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/sendButton"
app:layout_goneMarginBottom="57dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginBottom="56dp"
tools:ignore="MissingPrefix,RtlSymmetry" />
<!-- Constraints are updated programmatically -->
<FrameLayout
android:id="@+id/composerEditTextOuterBorder"
android:layout_width="0dp"
android:layout_height="0dp"
android:minHeight="40dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_marginHorizontal="12dp"
app:layout_constraintVertical_bias="0"
app:layout_constraintTop_toTopOf="parent"
@ -156,19 +156,19 @@
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
app:layout_constraintVertical_bias="0"
android:src="@drawable/ic_composer_full_screen"
android:background="?android:attr/selectableItemBackground"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/rich_text_editor_full_screen_toggle" />
<ImageButton
android:id="@+id/sendButton"
android:layout_width="56dp"
android:layout_height="60dp"
android:layout_height="56dp"
android:paddingEnd="4dp"
android:contentDescription="@string/action_send"
android:scaleType="center"
android:src="@drawable/ic_rich_composer_send"
android:visibility="invisible"
android:background="?android:selectableItemBackground"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -75,7 +75,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:overScrollMode="always"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/notificationAreaView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"

View File

@ -160,6 +160,28 @@ class OnboardingViewModelTest {
.finish()
}
@Test
fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest {
val test = viewModel.test()
fakeVectorFeatures.givenCombinedLoginEnabled()
givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE, canLoginWithQrCode = true)
viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn))
test
.assertStatesChanges(
initialState,
{ copy(onboardingFlow = OnboardingFlow.SignIn) },
{ copy(isLoading = true) },
{ copy(canLoginWithQrCode = true) },
{ copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE) },
{ copy(signMode = SignMode.SignIn) },
{ copy(isLoading = false) }
)
.assertEvents(OnboardingViewEvents.OpenCombinedLogin)
.finish()
}
@Test
fun `given can successfully login in with token, when logging in with token, then emits AccountSignedIn`() = runTest {
val test = viewModel.test()
@ -1152,11 +1174,13 @@ class OnboardingViewModelTest {
resultingState: SelectedHomeserverState,
config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG,
fingerprint: Fingerprint? = null,
canLoginWithQrCode: Boolean = false,
) {
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config)
fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration)
fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString())
fakeAuthenticationService.givenIsQrLoginSupported(config, canLoginWithQrCode)
}
private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) {
@ -1164,6 +1188,7 @@ class OnboardingViewModelTest {
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error))
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
fakeAuthenticationService.givenIsQrLoginSupported(A_HOMESERVER_CONFIG, false)
}
private fun givenUserNameIsAvailable(userName: String) {

View File

@ -58,6 +58,10 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
coEvery { getWellKnownData(matrixId, config) } returns result
}
fun givenIsQrLoginSupported(config: HomeServerConnectionConfig, result: Boolean) {
coEvery { isQrLoginSupported(config) } returns result
}
fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) {
coEvery { getWellKnownData(matrixId, config) } throws cause
}