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 id: checklist
attributes: attributes:
label: Release checklist 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: | placeholder: |
If you are reading this, you have deleted the content of the release template: undo the deletion or start again. If you are reading this, you have deleted the content of the release template: undo the deletion or start again.
value: | value: |

View file

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

View file

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

View file

@ -17,7 +17,8 @@ jobs:
contains(github.event.issue.labels.*.name, 'Z-IA') || contains(github.event.issue.labels.*.name, 'Z-IA') ||
contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || 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-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: steps:
- uses: actions/github-script@v5 - uses: actions/github-script@v5
with: 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) 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 moshi = "1.14.0"
def lifecycle = "2.5.1" def lifecycle = "2.5.1"
def flowBinding = "1.2.0" def flowBinding = "1.2.0"
def flipper = "0.174.0" def flipper = "0.176.0"
def epoxy = "5.0.0" def epoxy = "5.0.0"
def mavericks = "3.0.1" def mavericks = "3.0.1"
def glide = "4.14.2" 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 // 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 // the whole commit which set version 0.16.0-SNAPSHOT
def vanniktechEmoji = "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" def fragment = "1.5.4"
// Testing // 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 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219 distributionSha256Sum=312eb12875e1747e05c2f81a4789902d7e4ec5defbd1eefeaccc08acf096505d
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

12
gradlew vendored
View file

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (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. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -80,10 +80,10 @@ do
esac esac
done done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # This is normally unused
# shellcheck disable=SC2034
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} 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. # 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"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
@ -143,12 +143,16 @@ fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) 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 ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
case $MAX_FD in #( case $MAX_FD in #(
'' | soft) :;; #( '' | 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" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac

1
gradlew.bat vendored
View file

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

View file

@ -63,7 +63,7 @@ android {
// that the app's state is completely cleared between tests. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' 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", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""

View file

@ -19,32 +19,81 @@
# Ignore any error to not stop the script # Ignore any error to not stop the script
set +e set +e
printf "\n" printf "\n================================================================================\n"
printf "================================================================================\n"
printf "| Welcome to the release script! |\n" printf "| Welcome to the release script! |\n"
printf "================================================================================\n" printf "================================================================================\n"
releaseScriptLocation="${RELEASE_SCRIPT_PATH}" printf "Checking environment...\n"
envError=0
if [[ -z "${releaseScriptLocation}" ]]; then # Path of the key store (it's a file)
printf "Fatal: RELEASE_SCRIPT_PATH is not defined in the environment. Please set to the path of your local file 'releaseElement2.sh'.\n" keyStorePath="${ELEMENT_KEYSTORE_PATH}"
exit 1 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 fi
releaseScriptFullPath="${releaseScriptLocation}/releaseElement2.sh" if [ ${envError} == 1 ]; then
if [[ ! -f ${releaseScriptFullPath} ]]; then
printf "Fatal: release script not found at ${releaseScriptFullPath}.\n"
exit 1 exit 1
fi 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 # Guessing version to propose a default version
versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut -d " " -f3` versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut -d " " -f3`
versionMinorCandidate=`grep "ext.versionMinor" ./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` versionPatchCandidate=`grep "ext.versionPatch" ./vector-app/build.gradle | cut -d " " -f3`
versionCandidate="${versionMajorCandidate}.${versionMinorCandidate}.${versionPatchCandidate}" versionCandidate="${versionMajorCandidate}.${versionMinorCandidate}.${versionPatchCandidate}"
printf "\n"
read -p "Please enter the release version (example: ${versionCandidate}). Just press enter if ${versionCandidate} is correct. " version read -p "Please enter the release version (example: ${versionCandidate}). Just press enter if ${versionCandidate} is correct. " version
version=${version:-${versionCandidate}} version=${version:-${versionCandidate}}
@ -77,7 +126,7 @@ fi
cp ./vector-app/build.gradle ./vector-app/build.gradle.bak 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.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.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 rm ./vector-app/build.gradle.bak
cp ./matrix-sdk-android/build.gradle ./matrix-sdk-android/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 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} 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." 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}" git commit -a -m "Adding fastlane file for version ${version}"
printf "\n================================================================================\n" printf "\n================================================================================\n"
@ -184,7 +234,6 @@ git checkout develop
# Set next version # Set next version
printf "\n================================================================================\n" printf "\n================================================================================\n"
printf "Setting next version on file './vector-app/build.gradle'...\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 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 sed "s/ext.versionPatch = .*/ext.versionPatch = ${nextPatchVersion}/" ./vector-app/build.gradle.bak > ./vector-app/build.gradle
rm ./vector-app/build.gradle.bak rm ./vector-app/build.gradle.bak
@ -214,17 +263,93 @@ else
fi fi
printf "\n================================================================================\n" 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 "\n================================================================================\n"
printf "Running the release script...\n" printf "Downloading the artifact...\n"
cd ${releaseScriptLocation}
${releaseScriptFullPath} "v${version}" # Download files
cd - 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" printf "\n================================================================================\n"
apkPath="${releaseScriptLocation}/Element/v${version}/vector-gplay-arm64-v8a-release-signed.apk" printf "Unzipping the artifact...\n"
printf "Installing apk on a real device...\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} 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." 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." read -p "Add the 4 signed APKs to the GitHub release. Press enter when it's done."
printf "\n================================================================================\n" printf "\n================================================================================\n"
printf "Ping the Android Internal room. Here is an example of message which can be sent:\n\n" printf "Message for the Android internal room:\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" 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!"
read -p "Press enter when it's done." 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 "\n================================================================================\n"
printf "Congratulation! Kudos for using this script! Have a nice day!\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. // 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 // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
ext.versionPatch = 10 ext.versionPatch = 12
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'
@ -440,13 +440,13 @@ dependencies {
// Plant Timber tree for test // Plant Timber tree for test
androidTestImplementation libs.tests.timberJunitRule androidTestImplementation libs.tests.timberJunitRule
// "The one who serves a great Espresso" // "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' exclude group: 'org.jetbrains.kotlin'
} }
androidTestImplementation libs.mockk.mockkAndroid androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator androidTestUtil libs.androidx.orchestrator
androidTestImplementation libs.androidx.fragmentTesting 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 libs.androidx.fragmentTesting
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
} }

View file

@ -328,11 +328,11 @@ dependencies {
// Plant Timber tree for test // Plant Timber tree for test
androidTestImplementation libs.tests.timberJunitRule androidTestImplementation libs.tests.timberJunitRule
// "The one who serves a great Espresso" // "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' exclude group: 'org.jetbrains.kotlin'
} }
androidTestImplementation libs.mockk.mockkAndroid androidTestImplementation libs.mockk.mockkAndroid
androidTestUtil libs.androidx.orchestrator androidTestUtil libs.androidx.orchestrator
debugImplementation libs.androidx.fragmentTesting 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 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. // 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) { 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 { 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) { override fun onEnd(animation: WindowInsetsAnimationCompat) {
applyInsetsFromAnimation = false isAnimating = false
view.requestApplyInsets() view.requestApplyInsets()
} }
}) })
ViewCompat.setOnApplyWindowInsetsListener(view) { _: View, insets: WindowInsetsCompat -> ViewCompat.setOnApplyWindowInsetsListener(view) { _: View, insets: WindowInsetsCompat ->
if (!applyInsetsFromAnimation) { if (isAnimating) {
applyInsetsFromAnimation = true
applyInsets(view, insets)
} else {
insets insets
} else {
applyInsets(view, insets)
} }
} }

View file

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

View file

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

View file

@ -42,7 +42,6 @@ import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import im.vector.app.R 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.setTextIfDifferent
import im.vector.app.core.extensions.showKeyboard import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
@ -132,8 +131,6 @@ class RichTextComposerLayout @JvmOverloads constructor(
views.bottomSheetHandle.isVisible = isFullScreen views.bottomSheetHandle.isVisible = isFullScreen
if (isFullScreen) { if (isFullScreen) {
editText.showKeyboard(true) editText.showKeyboard(true)
} else {
editText.hideKeyboard()
} }
this.isFullScreen = isFullScreen 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.START, R.id.composerLayoutContent, ConstraintSet.START, dpToPx(12))
connect(R.id.composerEditTextOuterBorder, ConstraintSet.END, R.id.composerLayoutContent, ConstraintSet.END, dpToPx(12)) connect(R.id.composerEditTextOuterBorder, ConstraintSet.END, R.id.composerLayoutContent, ConstraintSet.END, dpToPx(12))
} else { } else {
connect(R.id.composerEditTextOuterBorder, ConstraintSet.TOP, R.id.composerLayoutContent, ConstraintSet.TOP, 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(10)) 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.START, R.id.attachmentButton, ConstraintSet.END, 0)
connect(R.id.composerEditTextOuterBorder, ConstraintSet.END, R.id.sendButton, ConstraintSet.START, 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()) { if (!vectorFeatures.isQrCodeLoginEnabled()) {
setState { setState {
copy( copy(
@ -133,16 +133,12 @@ class OnboardingViewModel @AssistedInject constructor(
) )
} }
} else { } else {
viewModelScope.launch { // check if selected server supports MSC3882 first
// check if selected server supports MSC3882 first val canLoginWithQrCode = authenticationService.isQrLoginSupported(config)
homeServerConnectionConfigFactory.create(homeServerUrl)?.let { setState {
val canLoginWithQrCode = authenticationService.isQrLoginSupported(it) copy(
setState { canLoginWithQrCode = canLoginWithQrCode
copy( )
canLoginWithQrCode = canLoginWithQrCode
)
}
}
} }
} }
} }
@ -710,7 +706,6 @@ class OnboardingViewModel @AssistedInject constructor(
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
} else { } else {
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction) startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction)
checkQrCodeLoginCapability(homeServerConnectionConfig.homeServerUri.toString())
} }
} }
@ -769,6 +764,8 @@ class OnboardingViewModel @AssistedInject constructor(
_viewEvents.post(OnboardingViewEvents.OutdatedHomeserver) _viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
} }
checkQrCodeLoginCapability(config)
when (trigger) { when (trigger) {
is OnboardingAction.HomeServerChange.SelectHomeServer -> { is OnboardingAction.HomeServerChange.SelectHomeServer -> {
onHomeServerSelected(config, serverTypeOverride, authResult) onHomeServerSelected(config, serverTypeOverride, authResult)

View file

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

View file

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

View file

@ -160,6 +160,28 @@ class OnboardingViewModelTest {
.finish() .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 @Test
fun `given can successfully login in with token, when logging in with token, then emits AccountSignedIn`() = runTest { fun `given can successfully login in with token, when logging in with token, then emits AccountSignedIn`() = runTest {
val test = viewModel.test() val test = viewModel.test()
@ -1152,11 +1174,13 @@ class OnboardingViewModelTest {
resultingState: SelectedHomeserverState, resultingState: SelectedHomeserverState,
config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG, config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG,
fingerprint: Fingerprint? = null, fingerprint: Fingerprint? = null,
canLoginWithQrCode: Boolean = false,
) { ) {
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config) fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config)
fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration)
fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString()) fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString())
fakeAuthenticationService.givenIsQrLoginSupported(config, canLoginWithQrCode)
} }
private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) { private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) {
@ -1164,6 +1188,7 @@ class OnboardingViewModelTest {
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error)) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error))
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString()) fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
fakeAuthenticationService.givenIsQrLoginSupported(A_HOMESERVER_CONFIG, false)
} }
private fun givenUserNameIsAvailable(userName: String) { private fun givenUserNameIsAvailable(userName: String) {

View file

@ -58,6 +58,10 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
coEvery { getWellKnownData(matrixId, config) } returns result 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) { fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) {
coEvery { getWellKnownData(matrixId, config) } throws cause coEvery { getWellKnownData(matrixId, config) } throws cause
} }