Merge branch 'merge_v1.2.1' into sc

Change-Id: I003540479c02eae32fdeee7230ef11e5d373e9b0
This commit is contained in:
SpiritCroc 2021-09-09 11:30:00 +02:00
commit 25b11084f4
462 changed files with 9665 additions and 3454 deletions

View file

@ -1,3 +1,53 @@
Changes in Element v1.2.1 (2021-09-08)
======================================
Features ✨
----------
- Support Android 11 Conversation features ([#1809](https://github.com/vector-im/element-android/issues/1809))
- Introduces AutoAcceptInvites which can be enabled at compile time. ([#3531](https://github.com/vector-im/element-android/issues/3531))
- New call designs ([#3599](https://github.com/vector-im/element-android/issues/3599))
- Restricted Join Rule | Inform admins of new option ([#3631](https://github.com/vector-im/element-android/issues/3631))
- Mention and Keyword Notification Settings: Turn on/off keyword notifications and edit keywords. ([#3650](https://github.com/vector-im/element-android/issues/3650))
- Support accept 3pid invite when email is not bound to account ([#3691](https://github.com/vector-im/element-android/issues/3691))
- Space summary pagination ([#3693](https://github.com/vector-im/element-android/issues/3693))
- Update Email invite to be aware of spaces ([#3695](https://github.com/vector-im/element-android/issues/3695))
- M11.12 Spaces | Default to 'Home' in settings ([#3754](https://github.com/vector-im/element-android/issues/3754))
- Call: show dialog for some ended reasons. ([#3853](https://github.com/vector-im/element-android/issues/3853))
- Add expired account error code in the matrix SDK ([#3900](https://github.com/vector-im/element-android/issues/3900))
- Add password errors in the matrix SDK ([#3927](https://github.com/vector-im/element-android/issues/3927))
- Upgrade AGP to 7.0.2.
When compiling using command line, make sure to use the JDK 11 by adding for instance `-Dorg.gradle.java.home=/Applications/Android\ Studio\ Preview.app/Contents/jre/Contents/Home` or by setting JAVA_HOME. ([#3954](https://github.com/vector-im/element-android/issues/3954))
- Check power level before displaying actions in the room details' timeline ([#3959](https://github.com/vector-im/element-android/issues/3959))
Bugfixes 🐛
----------
- Add mxid to autocomplete suggestion if more than one user in a room has the same displayname ([#1823](https://github.com/vector-im/element-android/issues/1823))
- Use WebView cache for widgets to avoid excessive data use ([#2648](https://github.com/vector-im/element-android/issues/2648))
- Jitsi-hosted jitsi conferences not loading ([#2846](https://github.com/vector-im/element-android/issues/2846))
- Space Explore Rooms no feedback on failed to join ([#3207](https://github.com/vector-im/element-android/issues/3207))
- Notifications - Fix missing sound on notifications. ([#3243](https://github.com/vector-im/element-android/issues/3243))
- the element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid ([#3735](https://github.com/vector-im/element-android/issues/3735))
- Update the AccountData with the users' matrix Id instead of their email for those invited by email in a direct chat ([#3743](https://github.com/vector-im/element-android/issues/3743))
- Send an empty body for POST rooms/{roomId}/receipt/{receiptType}/{eventId} ([#3789](https://github.com/vector-im/element-android/issues/3789))
- Fix order in which the items of the attachment menu appear ([#3793](https://github.com/vector-im/element-android/issues/3793))
- Authenticated Jitsi not working in release ([#3841](https://github.com/vector-im/element-android/issues/3841))
- Home: Dial pad lost entry when config changes ([#3845](https://github.com/vector-im/element-android/issues/3845))
- Message edition is not rendered in e2e rooms after pagination ([#3887](https://github.com/vector-im/element-android/issues/3887))
- Crash on opening a room on Android 5.0 and 5.1 - Regression with Voice message ([#3897](https://github.com/vector-im/element-android/issues/3897))
- Fix a crash at start-up if translated string is empty ([#3910](https://github.com/vector-im/element-android/issues/3910))
- PushRule enabling request is not following the spec ([#3911](https://github.com/vector-im/element-android/issues/3911))
- Enable image preview in Android's share sheet (Android 11+) ([#3965](https://github.com/vector-im/element-android/issues/3965))
- Voice Message - Cannot render voice message if the waveform data is corrupted ([#3983](https://github.com/vector-im/element-android/issues/3983))
- Fix memory leak on RoomDetailFragment (ValueAnimator) ([#3990](https://github.com/vector-im/element-android/issues/3990))
Other changes
-------------
- VoIP: Merge virtual room timeline in corresponding native room (call events only). ([#3520](https://github.com/vector-im/element-android/issues/3520))
- Issue templates: modernise and sync with element-web ([#3883](https://github.com/vector-im/element-android/issues/3883))
- Issue templates: modernise SDK and release checklists, and add homeserver question for bugs ([#3889](https://github.com/vector-im/element-android/issues/3889))
- Issue templates: merge expected and actual results ([#3960](https://github.com/vector-im/element-android/issues/3960))
Changes in Element v1.2.0 (2021-08-12)
======================================

View file

@ -116,9 +116,34 @@ You should consider adding Unit tests with your PR, and also integration tests (
### Internationalisation
When adding new string resources, please only add new entries in file `value/strings.xml`. Translations will be added later by the community of translators with a specific tool named [Weblate](https://translate.riot.im/projects/riot-android/).
Translations are handled using an external tool: [Weblate](https://translate.element.io/projects/element-android/)
As a general rule, please never edit or add or remove translations to the project in a Pull Request. It can lead to merge conflict if the translations are also modified in Weblate side.
#### Adding new string
When adding new string resources, please only add new entries in file `value/strings.xml`. Translations will be added later by the community of translators using Weblate.
New strings can be added anywhere in the file `value/strings.xml`, not necessarily at the end of the file. Generally, it's even better to add the new strings in some dedicated section per feature, and not at the end of the file, to avoid merge conflict between 2 PR adding strings at the end of the same file.
Do not hesitate to use plurals when appropriate.
#### Editing existing strings
Two cases:
- If the meaning stays the same, it's OK to edit the original string (i.e. the English version).
- If the meaning is not the same, please create a new string and do not remove the existing string. See below for instructions to remove existing string.
#### Removing existing strings
If a string is not used anymore, it should be removed from the resource, but please do not remove the strings or its translations in the PR. It can lead to merge conflict with Weblate, and to lint error if new translations from deleted strings are added with Weblate.
Instead, please comment the original string with:
```xml
<!-- TO BE REMOVED -->
```
The string will be removed during the next sync with Weblate.
### Accessibility
Please consider accessibility as an important point. As a minimum requirement, in layout XML files please use attributes such as `android:contentDescription` and `android:importantForAccessibility`, and test with a screen reader if it's working well. You can add new string resources, dedicated to accessibility, in this case, please prefix theirs id with `a11y_`.

View file

@ -34,11 +34,11 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = "11"
}
buildFeatures {

View file

@ -1,21 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:visibility="visible"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:visibility="visible" />
<ProgressBar
android:layout_centerInParent="true"
android:id="@+id/imageLoaderProgress"
style="?android:attr/progressBarStyle"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerInParent="true"
android:visibility="visible"
tools:visibility="gone" />

View file

@ -9,6 +9,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:importantForAccessibility="no"
android:scaleType="centerInside" />
<VideoView
@ -22,6 +23,7 @@
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_centerInParent="true"
android:contentDescription="@string/a11y_play_pause"
android:visibility="gone"
tools:visibility="visible" />

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- TODO Create a dedicated module for translations to be able to translate this string like the others (See #3955) -->
<string name="a11y_play_pause">Play or pause the video</string>
</resources>

View file

@ -12,8 +12,10 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
classpath 'com.google.gms:google-services:4.3.8'
// Release notes of Android Gradle Plugin (AGP):
// https://developer.android.com/studio/releases/gradle-plugin
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'com.google.gms:google-services:4.3.10'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4'
@ -44,6 +46,9 @@ allprojects {
includeGroupByRegex 'com\\.github\\.chrisbanes'
// PFLockScreen-Android
includeGroupByRegex 'com\\.github\\.vector-im'
// DraggableView
includeGroupByRegex 'com\\.github\\.hyuwah'
// UnifiedPush
includeGroupByRegex 'com\\.github\\.UnifiedPush'

View file

@ -3,6 +3,3 @@ apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "8"
targetCompatibility = "8"

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=9bb8bc05f562f2d42bdf1ba8db62f6b6fa1c3bf6c392228802cc7cb0578fe7e0
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip
distributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

269
gradlew vendored
View file

@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -17,67 +17,101 @@
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# 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
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
# 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"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@ -106,80 +140,95 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View file

@ -39,13 +39,16 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = "11"
}
buildFeatures {
viewBinding true
}

View file

@ -1,2 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="im.vector.lib.ui.styles"/>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.lib.ui.styles">
<application android:supportsRtl="true" />
</manifest>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/color_primary_alpha25" android:state_enabled="false" />
<item android:color="?colorPrimary" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.25" android:color="?attr/colorOnPrimary" android:state_enabled="false" />
<item android:color="?attr/colorOnPrimary" />
</selector>

View file

@ -2,7 +2,8 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
tools:ignore="UselessParent">
<!-- Inspired from https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/layout/progress_dialog.xml -->
<LinearLayout

View file

@ -28,6 +28,7 @@
<!-- Other useful color -->
<!-- Emoji text has to use a black text color -->
<color name="emoji_color">@android:color/black</color>
<color name="join_conference_animated_color">#0BAC7E</color>
<color name="half_transparent_status_bar">#80000000</color>

View file

@ -28,6 +28,10 @@
<dimen name="pill_min_height">20dp</dimen>
<dimen name="pill_text_padding">4dp</dimen>
<dimen name="call_pip_height">128dp</dimen>
<dimen name="call_pip_width">88dp</dimen>
<dimen name="call_pip_radius">8dp</dimen>
<dimen name="item_form_min_height">76dp</dimen>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="vctr_keyword_style" format="reference" />
<style name="Widget.Vector.Keyword" parent="Widget.MaterialComponents.Chip.Action">
<item name="android:textAppearance">@style/TextAppearance.Vector.Body</item>
<item name="chipBackgroundColor">@color/keyword_background_selector</item>
<item name="closeIconTint">@color/keyword_foreground_selector</item>
<item name="closeIconVisible">true</item>
<item name="android:textColor">@color/keyword_foreground_selector</item>
<item name="android:clickable">true</item>
<item name="android:checkable">false</item>
</style>
</resources>

View file

@ -154,6 +154,8 @@
<item name="vctr_social_login_button_gitlab_style">@style/Widget.Vector.Button.Outlined.SocialLogin.Gitlab.Dark</item>
<item name="vctr_jump_to_unread_style">@style/Widget.Vector.JumpToUnread.Dark</item>
<!-- Keywords -->
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
<!-- Voice Message -->
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_dark</item>

View file

@ -157,6 +157,9 @@
<item name="vctr_jump_to_unread_style">@style/Widget.Vector.JumpToUnread.Light</item>
<!-- Keywords -->
<item name="vctr_keyword_style">@style/Widget.Vector.Keyword</item>
<!-- Voice Message -->
<item name="vctr_voice_message_toast_background">@color/vctr_voice_message_toast_background_light</item>
</style>

View file

@ -24,12 +24,12 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "11"
}
}
@ -44,5 +44,5 @@ dependencies {
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
// Logging
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.jakewharton.timber:timber:5.0.1'
}

View file

@ -9,7 +9,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:10.6.1"
classpath "io.realm:realm-gradle-plugin:10.8.0"
}
}
@ -67,17 +67,13 @@ android {
installOptions "-g"
}
lintOptions {
lintConfig file("lint.xml")
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "11"
}
sourceSets {
@ -112,7 +108,7 @@ dependencies {
def lifecycle_version = '2.2.0'
def arch_version = '2.1.0'
def markwon_version = '3.1.0'
def daggerVersion = '2.38'
def daggerVersion = '2.38.1'
def work_version = '2.5.0'
def retrofit_version = '2.9.0'
@ -141,7 +137,7 @@ dependencies {
implementation "ru.noties.markwon:core:$markwon_version"
// Image
implementation 'androidx.exifinterface:exifinterface:1.3.2'
implementation 'androidx.exifinterface:exifinterface:1.3.3'
// Database
implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'
@ -162,14 +158,14 @@ dependencies {
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
// Logging
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.jakewharton.timber:timber:5.0.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'
// Video compression
implementation 'com.otaliastudios:transcoder:0.10.3'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.28'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.31'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.5.1'

View file

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- Modify some severity -->
<!-- Resource -->
<issue id="MissingTranslation" severity="warning" />
<issue id="TypographyEllipsis" severity="error" />
<issue id="ImpliedQuantity" severity="warning" />
<!-- UX -->
<issue id="ButtonOrder" severity="error" />
<!-- Layout -->
<issue id="UnknownIdInLayout" severity="error" />
<issue id="StringFormatCount" severity="error" />
<issue id="HardcodedText" severity="error" />
<issue id="SpUsage" severity="error" />
<issue id="ObsoleteLayoutParam" severity="error" />
<issue id="InefficientWeight" severity="error" />
<issue id="DisableBaselineAlignment" severity="error" />
<issue id="ScrollViewSize" severity="error" />
<!-- RTL -->
<issue id="RtlEnabled" severity="error" />
<issue id="RtlHardcoded" severity="error" />
<issue id="RtlSymmetry" severity="error" />
<!-- Code -->
<issue id="SetTextI18n" severity="error" />
<issue id="ViewConstructor" severity="error" />
<issue id="UseValueOf" severity="error" />
<issue id="ObsoleteSdkInt" severity="error" />
</lint>

View file

@ -0,0 +1,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.matrix.android.sdk">
<application>
<!--
This is mandatory to run integration tests
-->
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove" />
</application>
</manifest>

View file

@ -0,0 +1,60 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
@FixMethodOrder(MethodSorters.JVM)
class PermalinkParserTest {
@Test
fun testParseEmailInvite() {
val rawInvite = """
https://app.element.io/#/room/%21MRBNLPtFnMAazZVPMO%3Amatrix.org?email=bob%2Bspace%40example.com&signurl=https%3A%2F%2Fvector.im%2F_matrix%2Fidentity%2Fapi%2Fv1%2Fsign-ed25519%3Ftoken%3DXmOwRZnSFabCRhTywFbJWKXWVNPysOpXIbroMGaUymqkJSvHeVKRsjHajwjCYdBsvGSvHauxbKfJmOxtXldtyLnyBMLKpBQCMzyYggrdapbVIceWZBtmslOQrXLABRoe%26private_key%3DT2gq0c3kJB_8OroXVxl1pBnzHsN7V6Xn4bEBSeW1ep4&room_name=Team2&room_avatar_url=&inviter_name=hiphop5&guest_access_token=&guest_user_id=
""".trimIndent()
.replace("https://app.element.io/#/room/", "https://matrix.to/#/")
val parsedLink = PermalinkParser.parse(rawInvite)
Assert.assertTrue("Should be parsed as email invite but was ${parsedLink::class.java}", parsedLink is PermalinkData.RoomEmailInviteLink)
parsedLink as PermalinkData.RoomEmailInviteLink
Assert.assertEquals("!MRBNLPtFnMAazZVPMO:matrix.org", parsedLink.roomId)
Assert.assertEquals("XmOwRZnSFabCRhTywFbJWKXWVNPysOpXIbroMGaUymqkJSvHeVKRsjHajwjCYdBsvGSvHauxbKfJmOxtXldtyLnyBMLKpBQCMzyYggrdapbVIceWZBtmslOQrXLABRoe", parsedLink.token)
Assert.assertEquals("vector.im", parsedLink.identityServer)
Assert.assertEquals("Team2", parsedLink.roomName)
Assert.assertEquals("hiphop5", parsedLink.inviterName)
}
@Test
fun testParseLinkWIthEvent() {
val rawInvite = "https://matrix.to/#/!OGEhHVWSdvArJzumhm:matrix.org/\$xuvJUVDJnwEeVjPx029rAOZ50difpmU_5gZk_T0jGfc?via=matrix.org&via=libera.chat&via=matrix.example.io"
val parsedLink = PermalinkParser.parse(rawInvite)
Assert.assertTrue("Should be parsed as room link", parsedLink is PermalinkData.RoomLink)
parsedLink as PermalinkData.RoomLink
Assert.assertEquals("!OGEhHVWSdvArJzumhm:matrix.org", parsedLink.roomIdOrAlias)
Assert.assertEquals("\$xuvJUVDJnwEeVjPx029rAOZ50difpmU_5gZk_T0jGfc", parsedLink.eventId)
Assert.assertEquals(3, parsedLink.viaParameters.size)
Assert.assertTrue(parsedLink.viaParameters.contains("matrix.example.io"))
Assert.assertTrue(parsedLink.viaParameters.contains("matrix.org"))
Assert.assertTrue(parsedLink.viaParameters.contains("matrix.example.io"))
}
}

View file

@ -91,6 +91,7 @@ class CommonTestHelper(context: Context) {
*
* @param session the session to sync
*/
@Suppress("EXPERIMENTAL_API_USAGE")
fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis) {
val lock = CountDownLatch(1)
@ -327,6 +328,7 @@ class CommonTestHelper(context: Context) {
assertTrue(latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
}
@Suppress("EXPERIMENTAL_API_USAGE")
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
GlobalScope.launch {
while (true) {

View file

@ -84,6 +84,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
/**
* @return alice and bob sessions
*/
@Suppress("EXPERIMENTAL_API_USAGE")
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom)
val aliceSession = cryptoTestData.firstSession
@ -255,6 +256,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
)
}
@Suppress("EXPERIMENTAL_API_USAGE")
fun createDM(alice: Session, bob: Session): String {
val roomId = mTestHelper.runBlockingTest {
alice.createDirectRoom(bob.myUserId)

View file

@ -60,6 +60,7 @@ class QuadSTests : InstrumentedTest {
}
@Test
@Suppress("EXPERIMENTAL_API_USAGE")
fun test_Generate4SKey() {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
@ -275,6 +276,7 @@ class QuadSTests : InstrumentedTest {
mTestHelper.signOutAndClose(aliceSession)
}
@Suppress("EXPERIMENTAL_API_USAGE")
private fun assertAccountData(session: Session, type: String): UserAccountDataEvent {
val accountDataLock = CountDownLatch(1)
var accountData: UserAccountDataEvent? = null

View file

@ -139,7 +139,7 @@ class TimelineForwardPaginationTest : InstrumentedTest {
// Alice can see the first event of the room (so Back pagination has worked)
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
// 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
&& snapshot.size == 6 + 1 + 50
&& snapshot.size == 57 // 6 + 1 + 50
}
aliceTimeline.addListener(aliceEventsListener)

View file

@ -189,7 +189,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
Timber.w(" event ${it.root}")
}
snapshot.size == 8 + 1 + 35
snapshot.size == 44 // 8 + 1 + 35
}
bobTimeline.addListener(eventsListener)
@ -218,7 +218,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
// Bob can see the first event of the room (so Back pagination has worked)
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
// 8 for room creation item 60 message from Alice
&& snapshot.size == 8 + 60
&& snapshot.size == 68 // 8 + 60
&& snapshot.checkSendOrder(secondMessage, 30, 0)
&& snapshot.checkSendOrder(firstMessage, 30, 30)
}

View file

@ -28,12 +28,9 @@ import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
@ -42,7 +39,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.space.JoinSpaceResult
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.SessionTestParams
@ -54,6 +50,7 @@ class SpaceCreationTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
@Test
@Suppress("EXPERIMENTAL_API_USAGE")
fun createSimplePublicSpace() {
val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true))
val roomName = "My Space"
@ -137,6 +134,7 @@ class SpaceCreationTest : InstrumentedTest {
}
@Test
@Suppress("EXPERIMENTAL_API_USAGE")
fun testSimplePublicSpaceWithChildren() {
val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true))
@ -162,7 +160,7 @@ class SpaceCreationTest : InstrumentedTest {
commonTestHelper.waitWithLatch {
GlobalScope.launch {
syncedSpace?.addChildren(firstChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", true, suggested = true)
syncedSpace?.addChildren(firstChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true)
it.countDown()
}
}
@ -181,7 +179,7 @@ class SpaceCreationTest : InstrumentedTest {
commonTestHelper.waitWithLatch {
GlobalScope.launch {
syncedSpace?.addChildren(secondChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", false, suggested = true)
syncedSpace?.addChildren(secondChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true)
it.countDown()
}
}
@ -202,19 +200,20 @@ class SpaceCreationTest : InstrumentedTest {
assertEquals("Room name should be set", roomName, spaceBobPov?.asRoom()?.roomSummary()?.name)
assertEquals("Room topic should be set", topic, spaceBobPov?.asRoom()?.roomSummary()?.topic)
// /!\ AUTO_JOIN has been descoped
// check if bob has joined automatically the first room
val bobMembershipFirstRoom = bobSession.getRoomSummary(firstChild!!)?.membership
assertEquals("Bob should have joined this room", Membership.JOIN, bobMembershipFirstRoom)
RoomSummaryQueryParams.Builder()
val childCount = bobSession.getRoomSummaries(
roomSummaryQueryParams {
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(spaceId)
}
).size
assertEquals("Unexpected number of joined children", 1, childCount)
// val bobMembershipFirstRoom = bobSession.getRoomSummary(firstChild!!)?.membership
// assertEquals("Bob should have joined this room", Membership.JOIN, bobMembershipFirstRoom)
// RoomSummaryQueryParams.Builder()
//
// val childCount = bobSession.getRoomSummaries(
// roomSummaryQueryParams {
// activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(spaceId)
// }
// ).size
//
// assertEquals("Unexpected number of joined children", 1, childCount)
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(bobSession)

View file

@ -47,6 +47,7 @@ class SpaceHierarchyTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
@Test
@Suppress("EXPERIMENTAL_API_USAGE")
fun createCanonicalChildRelation() {
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceName = "My Space"
@ -171,6 +172,7 @@ class SpaceHierarchyTest : InstrumentedTest {
// }
@Test
@Suppress("EXPERIMENTAL_API_USAGE")
fun testFilteringBySpace() {
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
@ -179,7 +181,7 @@ class SpaceHierarchyTest : InstrumentedTest {
Triple("A2", true, true)
))
val spaceBInfo = createPublicSpace(session, "SpaceB", listOf(
/* val spaceBInfo = */ createPublicSpace(session, "SpaceB", listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
@ -254,6 +256,7 @@ class SpaceHierarchyTest : InstrumentedTest {
}
@Test
@Suppress("EXPERIMENTAL_API_USAGE")
fun testBreakCycle() {
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
@ -301,6 +304,7 @@ class SpaceHierarchyTest : InstrumentedTest {
}
@Test
@Suppress("EXPERIMENTAL_API_USAGE")
fun testLiveFlatChildren() {
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
@ -389,6 +393,7 @@ class SpaceHierarchyTest : InstrumentedTest {
val roomIds: List<String>
)
@Suppress("EXPERIMENTAL_API_USAGE")
private fun createPublicSpace(session: Session,
spaceName: String,
childInfo: List<Triple<String, Boolean, Boolean?>>
@ -433,7 +438,7 @@ class SpaceHierarchyTest : InstrumentedTest {
fun testRootSpaces() {
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace(session, "SpaceA", listOf(
/* val spaceAInfo = */ createPublicSpace(session, "SpaceA", listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
))

View file

@ -10,15 +10,6 @@
<application android:networkSecurityConfig="@xml/network_security_config">
<!--
This is mandatory to run integration tests
-->
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove" />
<!--
The SDK offers a secured File provider to access downloaded files.
Access to these file will be given via the FileService, with a temporary

View file

@ -29,7 +29,9 @@ fun Throwable.is401() =
fun Throwable.isTokenError() =
this is Failure.ServerError
&& (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN)
&& (error.code == MatrixError.M_UNKNOWN_TOKEN
|| error.code == MatrixError.M_MISSING_TOKEN
|| error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT)
fun Throwable.shouldBeRetried(): Boolean {
return this is Failure.NetworkConnection

View file

@ -23,4 +23,5 @@ sealed class GlobalError {
data class InvalidToken(val softLogout: Boolean) : GlobalError()
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
data class CertificateError(val fingerprint: Fingerprint) : GlobalError()
object ExpiredAccount : GlobalError()
}

View file

@ -182,6 +182,24 @@ data class MatrixError(
/** (Not documented yet) */
const val M_WEAK_PASSWORD = "M_WEAK_PASSWORD"
/** The provided password's length is shorter than the minimum length required by the server. */
const val M_PASSWORD_TOO_SHORT = "M_PASSWORD_TOO_SHORT"
/** The password doesn't contain any digit but the server requires at least one. */
const val M_PASSWORD_NO_DIGIT = "M_PASSWORD_NO_DIGIT"
/** The password doesn't contain any uppercase letter but the server requires at least one. */
const val M_PASSWORD_NO_UPPERCASE = "M_PASSWORD_NO_UPPERCASE"
/** The password doesn't contain any lowercase letter but the server requires at least one. */
const val M_PASSWORD_NO_LOWERCASE = "M_PASSWORD_NO_LOWERCASE"
/** The password doesn't contain any symbol but the server requires at least one. */
const val M_PASSWORD_NO_SYMBOL = "M_PASSWORD_NO_SYMBOL"
/** The password was found in a dictionary, and is not acceptable. */
const val M_PASSWORD_IN_DICTIONARY = "M_PASSWORD_IN_DICTIONARY"
const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED"
// For identity service
@ -189,5 +207,12 @@ data class MatrixError(
// Possible value for "limit_type"
const val LIMIT_TYPE_MAU = "monthly_active_user"
/**
* The user account has expired. It has to be renewed by clicking on an email or by sending a renewal token.
*
* More documentation can be found in the dedicated Synapse plugin module repository: https://github.com/matrix-org/synapse-email-account-validity
*/
const val ORG_MATRIX_EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT"
}
}

View file

@ -15,6 +15,7 @@
*/
package org.matrix.android.sdk.api.pushrules
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
import org.matrix.android.sdk.api.session.events.model.Event
@ -39,7 +40,7 @@ interface PushRuleService {
suspend fun updatePushRuleActions(kind: RuleKind, ruleId: String, enable: Boolean, actions: List<Action>?)
suspend fun removePushRule(kind: RuleKind, pushRule: PushRule)
suspend fun removePushRule(kind: RuleKind, ruleId: String)
fun addPushRuleListener(listener: PushRuleListener)
@ -56,4 +57,6 @@ interface PushRuleService {
fun onEventRedacted(redactedEventId: String)
fun batchFinish()
}
fun getKeywords(): LiveData<Set<String>>
}

View file

@ -35,6 +35,11 @@ object RuleIds {
// Default Content Rules
const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name"
// The keywords rule id is not a "real" id in that it does not exist server-side.
// It is used client-side as a placeholder for rendering the keyword push rule setting
// alongside the others. A similar approach and naming is used on Web and iOS.
const val RULE_ID_KEYWORDS = "_keywords"
// Default Underride Rules
const val RULE_ID_CALL = ".m.rule.call"
const val RULE_ID_ONE_TO_ONE_ENCRYPTED_ROOM = ".m.rule.encrypted_room_one_to_one"

View file

@ -52,7 +52,7 @@ interface VerificationService {
transactionId: String?): String?
/**
* Request a key verification from another user using toDevice events.
* Request key verification with another user via room events (instead of the to-device API)
*/
fun requestKeyVerificationInDMs(methods: List<VerificationMethod>,
otherUserId: String,

View file

@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.session.identity
import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
/**
* Provides access to the identity server configuration and services identity server can provide
*/
@ -121,6 +123,18 @@ interface IdentityService {
*/
suspend fun getShareStatus(threePids: List<ThreePid>): Map<ThreePid, SharedState>
/**
* When one performs a 3pid invite and the third party identifier is unknown, the home server
* will store the invitation in the Identity server and store some information in the room state membership event.
* The email invite will contains the token and secret that can be used to claim the stored invitation
*
* To aid clients who may not be able to perform crypto themselves,
* the identity server offers some crypto functionality to help in accepting invitations.
* This is less secure than the client doing it itself, but may be useful where this isn't possible.
*/
suspend fun sign3pidInvitation(identiyServer: String, token: String, secret: String) : SignInvitationResult
fun addListener(listener: IdentityServiceListener)
fun removeListener(listener: IdentityServiceListener)
}

View file

@ -17,6 +17,8 @@
package org.matrix.android.sdk.api.session.permalinks
import android.net.Uri
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* This sealed class represents all the permalink cases.
@ -31,6 +33,25 @@ sealed class PermalinkData {
val viaParameters: List<String>
) : PermalinkData()
/**
* &room_name=Team2
&room_avatar_url=mxc:
&inviter_name=bob
*/
@Parcelize
data class RoomEmailInviteLink(
val roomId: String,
val email: String,
val signUrl: String,
val roomName: String?,
val roomAvatarUrl: String?,
val inviterName: String?,
val identityServer: String,
val token: String,
val privateKey: String,
val roomType: String?
) : PermalinkData(), Parcelable
data class UserLink(val userId: String) : PermalinkData()
data class GroupLink(val groupId: String) : PermalinkData()

View file

@ -19,14 +19,18 @@ package org.matrix.android.sdk.api.session.permalinks
import android.net.Uri
import android.net.UrlQuerySanitizer
import org.matrix.android.sdk.api.MatrixPatterns
import timber.log.Timber
import java.net.URLDecoder
/**
* This class turns an uri to a [PermalinkData]
* This class turns a uri to a [PermalinkData]
* element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks
* or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org)
*/
object PermalinkParser {
/**
* Turns an uri string to a [PermalinkData]
* Turns a uri string to a [PermalinkData]
*/
fun parse(uriString: String): PermalinkData {
val uri = Uri.parse(uriString)
@ -34,13 +38,16 @@ object PermalinkParser {
}
/**
* Turns an uri to a [PermalinkData]
* Turns a uri to a [PermalinkData]
* https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md
*/
fun parse(uri: Uri): PermalinkData {
if (!uri.toString().startsWith(PermalinkService.MATRIX_TO_URL_BASE)) {
return PermalinkData.FallbackLink(uri)
}
val fragment = uri.fragment
// We can't use uri.fragment as it is decoding to early and it will break the parsing
// of parameters that represents url (like signurl)
val fragment = uri.toString().substringAfter("#") // uri.fragment
if (fragment.isNullOrEmpty()) {
return PermalinkData.FallbackLink(uri)
}
@ -51,21 +58,23 @@ object PermalinkParser {
val params = safeFragment
.split(MatrixPatterns.SEP_REGEX)
.filter { it.isNotEmpty() }
.map { URLDecoder.decode(it, "UTF-8") }
.take(2)
val identifier = params.getOrNull(0)
// the element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the
// mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid
var identifier = params.getOrNull(0)
if (identifier.equals("user")) {
identifier = params.getOrNull(1)
}
val extraParameter = params.getOrNull(1)
return when {
identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> {
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = false,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters
)
handleRoomIdCase(fragment, identifier, uri, extraParameter, viaQueryParameters)
}
MatrixPatterns.isRoomAlias(identifier) -> {
PermalinkData.RoomLink(
@ -79,13 +88,59 @@ object PermalinkParser {
}
}
private fun handleRoomIdCase(fragment: String, identifier: String, uri: Uri, extraParameter: String?, viaQueryParameters: List<String>): PermalinkData {
// Can't rely on built in parsing because it's messing around the signurl
val paramList = safeExtractParams(fragment)
val signUrl = paramList.firstOrNull { it.first == "signurl" }?.second
val email = paramList.firstOrNull { it.first == "email" }?.second
return if (signUrl.isNullOrEmpty().not() && email.isNullOrEmpty().not()) {
try {
val signValidUri = Uri.parse(signUrl)
val identityServerHost = signValidUri.authority ?: throw IllegalArgumentException()
val token = signValidUri.getQueryParameter("token") ?: throw IllegalArgumentException()
val privateKey = signValidUri.getQueryParameter("private_key") ?: throw IllegalArgumentException()
PermalinkData.RoomEmailInviteLink(
roomId = identifier,
email = email!!,
signUrl = signUrl!!,
roomName = paramList.firstOrNull { it.first == "room_name" }?.second,
inviterName = paramList.firstOrNull { it.first == "inviter_name" }?.second,
roomAvatarUrl = paramList.firstOrNull { it.first == "room_avatar_url" }?.second,
roomType = paramList.firstOrNull { it.first == "room_type" }?.second,
identityServer = identityServerHost,
token = token,
privateKey = privateKey
)
} catch (failure: Throwable) {
Timber.i("## Permalink: Failed to parse permalink $signUrl")
PermalinkData.FallbackLink(uri)
}
} else {
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = false,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters
)
}
}
private fun safeExtractParams(fragment: String) = fragment.substringAfter("?").split('&').mapNotNull {
val splitNameValue = it.split("=")
if (splitNameValue.size == 2) {
Pair(splitNameValue[0], URLDecoder.decode(splitNameValue[1], "UTF-8"))
} else null
}
private fun String.getViaParameters(): List<String> {
return UrlQuerySanitizer(this)
.parameterList
.filter {
it.mParameter == "via"
}.map {
it.mValue
it.mValue.let {
URLDecoder.decode(it, "UTF-8")
}
}
}
}

View file

@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
/**
@ -63,6 +64,18 @@ interface RoomService {
reason: String? = null,
viaServers: List<String> = emptyList())
/**
* @param roomId the roomId of the room to join
* @param reason optional reason for joining the room
* @param thirdPartySigned A signature of an m.third_party_invite token to prove that this user owns a third party identity
* which has been invited to the room.
*/
suspend fun joinRoom(
roomId: String,
reason: String? = null,
thirdPartySigned: SignInvitationResult
)
/**
* Get a room from a roomId
* @param roomId the roomId to look for.

View file

@ -19,5 +19,6 @@ package org.matrix.android.sdk.api.session.room
enum class RoomSortOrder {
NAME,
ACTIVITY,
PRIORITY_AND_ACTIVITY,
NONE
}

View file

@ -23,11 +23,15 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RoomJoinRulesAllowEntry(
/**
* space: The room ID of the space to check the membership of.
* The room ID to check the membership of.
*/
@Json(name = "space") val spaceID: String,
@Json(name = "room_id") val roomId: String?,
/**
* via: A list of servers which may be used to peek for membership of the space.
* "m.room_membership" to describe that we are allowing access via room membership. Future MSCs may define other types.
*/
@Json(name = "via") val via: List<String>
)
@Json(name = "type") val type: String?
) {
companion object {
fun restrictedToRoom(roomId: String) = RoomJoinRulesAllowEntry(roomId, "m.room_membership")
}
}

View file

@ -29,7 +29,9 @@ import timber.log.Timber
data class RoomJoinRulesContent(
@Json(name = "join_rule") val _joinRules: String? = null,
/**
* If the allow key is an empty list (or not a list at all), then the room reverts to standard public join rules
* If the allow key is an empty list (or not a list at all),
* then no users are allowed to join without an invite.
* Each entry is expected to be an object with the following keys:
*/
@Json(name = "allow") val allowList: List<RoomJoinRulesAllowEntry>? = null
) {

View file

@ -27,7 +27,7 @@ data class SpaceChildInfo(
val avatarUrl: String?,
val order: String?,
val activeMemberCount: Int?,
val autoJoin: Boolean,
// val autoJoin: Boolean,
val viaServers: List<String>,
val parentRoomId: String?,
val suggested: Boolean?,

View file

@ -32,5 +32,5 @@ data class AudioWaveformInfo(
* List of integers between zero and 1024, inclusive.
*/
@Json(name = "waveform")
val waveform: List<Int>? = null
val waveform: List<Int?>? = null
)

View file

@ -36,7 +36,7 @@ interface Space {
suspend fun addChildren(roomId: String,
viaServers: List<String>?,
order: String?,
autoJoin: Boolean = false,
// autoJoin: Boolean = false,
suggested: Boolean? = false)
fun getChildInfo(roomId: String): SpaceChildContent?
@ -46,8 +46,8 @@ interface Space {
@Throws
suspend fun setChildrenOrder(roomId: String, order: String?)
@Throws
suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean)
// @Throws
// suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean)
@Throws
suspend fun setChildrenSuggested(roomId: String, suggested: Boolean)

View file

@ -0,0 +1,28 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.space
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
data class SpaceHierarchyData(
val rootSummary: RoomSummary,
val children: List<SpaceChildInfo>,
val childrenState: List<Event>,
val nextToken: String? = null
)

View file

@ -18,9 +18,9 @@ package org.matrix.android.sdk.api.session.space
import android.net.Uri
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
typealias SpaceSummaryQueryParams = RoomSummaryQueryParams
@ -58,10 +58,17 @@ interface SpaceService {
/**
* Get's information of a space by querying the server
* @param suggestedOnly If true, return only child events and rooms where the m.space.child event has suggested: true.
* @param limit a client-defined limit to the maximum number of rooms to return per page. Must be a non-negative integer.
* @param from: Optional. Pagination token given to retrieve the next set of rooms. Note that if a pagination token is provided,
* then the parameters given for suggested_only and max_depth must be the same.
*/
suspend fun querySpaceChildren(spaceId: String,
suggestedOnly: Boolean? = null,
autoJoinedOnly: Boolean? = null): Pair<RoomSummary, List<SpaceChildInfo>>
limit: Int? = null,
from: String? = null,
// when paginating, pass back the m.space.child state events
knownStateList: List<Event>? = null): SpaceHierarchyData
/**
* Get a live list of space summaries. This list is refreshed as soon as the data changes.

View file

@ -40,12 +40,12 @@ data class SpaceChildContent(
* or consist of more than 50 characters, are forbidden and should be ignored if received.)
*/
@Json(name = "order") val order: String? = null,
/**
* The auto_join flag on a child listing allows a space admin to list the sub-spaces and rooms in that space which should
* be automatically joined by members of that space.
* (This is not a force-join, which are descoped for a future MSC; the user can subsequently part these room if they desire.)
*/
@Json(name = "auto_join") val autoJoin: Boolean? = false,
// /**
// * The auto_join flag on a child listing allows a space admin to list the sub-spaces and rooms in that space which should
// * be automatically joined by members of that space.
// * (This is not a force-join, which are descoped for a future MSC; the user can subsequently part these room if they desire.)
// */
// @Json(name = "auto_join") val autoJoin: Boolean? = false,
/**
* If `suggested` is set to `true`, that indicates that the child should be advertised to

View file

@ -23,12 +23,12 @@ import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
@ -43,6 +43,9 @@ internal class CancelGossipRequestWorker(context: Context,
override val sessionId: String,
val requestId: String,
val recipients: Map<String, List<String>>,
// The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided
// to use the same value if this worker is retried.
val txnId: String? = null,
override val lastFailureMessage: String? = null
) : SessionWorkerParams {
companion object {
@ -51,6 +54,7 @@ internal class CancelGossipRequestWorker(context: Context,
sessionId = sessionId,
requestId = request.requestId,
recipients = request.recipients,
txnId = createUniqueTxnId(),
lastFailureMessage = null
)
}
@ -66,7 +70,10 @@ internal class CancelGossipRequestWorker(context: Context,
}
override suspend fun doSafeWork(params: Params): Result {
val localId = LocalEcho.createLocalEchoId()
// params.txnId should be provided in all cases now. But Params can be deserialized by
// the WorkManager from data serialized in a previous version of the application, so without the txnId field.
// So if not present, we create a txnId
val txnId = params.txnId ?: createUniqueTxnId()
val contentMap = MXUsersDevicesMap<Any>()
val toDeviceContent = ShareRequestCancellation(
requestingDeviceId = credentials.deviceId,
@ -92,7 +99,7 @@ internal class CancelGossipRequestWorker(context: Context,
SendToDeviceTask.Params(
eventType = EventType.ROOM_KEY_REQUEST,
contentMap = contentMap,
transactionId = localId
transactionId = txnId
)
)
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED)

View file

@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
@ -356,7 +357,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
secretValue = secretValue,
requestUserId = request.userId,
requestDeviceId = request.deviceId,
requestId = request.requestId
requestId = request.requestId,
txnId = createUniqueTxnId()
)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
@ -376,13 +378,13 @@ internal class IncomingGossipingRequestManager @Inject constructor(
}
request.share = { secretValue ->
val params = SendGossipWorker.Params(
sessionId = userId,
secretValue = secretValue,
requestUserId = request.userId,
requestDeviceId = request.deviceId,
requestId = request.requestId
requestId = request.requestId,
txnId = createUniqueTxnId()
)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.di.SessionId
@ -26,6 +25,8 @@ import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper
import timber.log.Timber
import javax.inject.Inject
@ -131,7 +132,8 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
val params = SendGossipRequestWorker.Params(
sessionId = sessionId,
keyShareRequest = request as? OutgoingRoomKeyRequest,
secretShareRequest = request as? OutgoingSecretRequest
secretShareRequest = request as? OutgoingSecretRequest,
txnId = createUniqueTxnId()
)
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING)
val workRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
@ -154,7 +156,8 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
if (resend) {
val reSendParams = SendGossipRequestWorker.Params(
sessionId = sessionId,
keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId())
keyShareRequest = request.copy(requestId = RequestIdHelper.createUniqueRequestId()),
txnId = createUniqueTxnId()
)
val reSendWorkRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
gossipingWorkManager.postWork(reSendWorkRequest)

View file

@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
@ -31,6 +30,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
@ -46,6 +46,9 @@ internal class SendGossipRequestWorker(context: Context,
override val sessionId: String,
val keyShareRequest: OutgoingRoomKeyRequest? = null,
val secretShareRequest: OutgoingSecretRequest? = null,
// The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided
// to use the same value if this worker is retried.
val txnId: String? = null,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@ -58,7 +61,10 @@ internal class SendGossipRequestWorker(context: Context,
}
override suspend fun doSafeWork(params: Params): Result {
val localId = LocalEcho.createLocalEchoId()
// params.txnId should be provided in all cases now. But Params can be deserialized by
// the WorkManager from data serialized in a previous version of the application, so without the txnId field.
// So if not present, we create a txnId
val txnId = params.txnId ?: createUniqueTxnId()
val contentMap = MXUsersDevicesMap<Any>()
val eventType: String
val requestId: String
@ -122,7 +128,7 @@ internal class SendGossipRequestWorker(context: Context,
SendToDeviceTask.Params(
eventType = eventType,
contentMap = contentMap,
transactionId = localId
transactionId = txnId
)
)
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT)

View file

@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
@ -31,6 +30,7 @@ import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
@ -48,6 +48,9 @@ internal class SendGossipWorker(context: Context,
val requestUserId: String?,
val requestDeviceId: String?,
val requestId: String?,
// The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided
// to use the same value if this worker is retried.
val txnId: String? = null,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@ -62,7 +65,10 @@ internal class SendGossipWorker(context: Context,
}
override suspend fun doSafeWork(params: Params): Result {
val localId = LocalEcho.createLocalEchoId()
// params.txnId should be provided in all cases now. But Params can be deserialized by
// the WorkManager from data serialized in a previous version of the application, so without the txnId field.
// So if not present, we create a txnId
val txnId = params.txnId ?: createUniqueTxnId()
val eventType: String = EventType.SEND_SECRET
val toDeviceContent = SecretSendEventContent(
@ -127,7 +133,7 @@ internal class SendGossipWorker(context: Context,
SendToDeviceTask.Params(
eventType = EventType.ENCRYPTED,
contentMap = sendToDeviceMap,
transactionId = localId
transactionId = txnId
)
)
cryptoStore.updateGossipingRequestState(

View file

@ -27,7 +27,6 @@ import io.realm.Sort
import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
@ -88,6 +87,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.query.delete
import org.matrix.android.sdk.internal.crypto.store.db.query.get
import org.matrix.android.sdk.internal.crypto.store.db.query.getById
import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate
import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
import org.matrix.android.sdk.internal.di.CryptoDatabase
@ -1123,7 +1123,7 @@ internal class RealmCryptoStore @Inject constructor(
if (existing == null) {
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
this.requestId = LocalEcho.createLocalEchoId()
this.requestId = RequestIdHelper.createUniqueRequestId()
this.setRecipients(recipients)
this.requestState = OutgoingGossipingRequestState.UNSENT
this.type = GossipRequestType.KEY
@ -1153,7 +1153,7 @@ internal class RealmCryptoStore @Inject constructor(
this.type = GossipRequestType.SECRET
setRecipients(recipients)
this.requestState = OutgoingGossipingRequestState.UNSENT
this.requestId = LocalEcho.createLocalEchoId()
this.requestId = RequestIdHelper.createUniqueRequestId()
this.requestedInfoStr = secretName
}.toOutgoingGossipingRequest() as? OutgoingSecretRequest
} else {

View file

@ -22,8 +22,8 @@ import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import java.util.UUID
import javax.inject.Inject
import kotlin.random.Random
internal interface SendToDeviceTask : Task<SendToDeviceTask.Params, Unit> {
data class Params(
@ -31,7 +31,7 @@ internal interface SendToDeviceTask : Task<SendToDeviceTask.Params, Unit> {
val eventType: String,
// the content to send. Map from user_id to device_id to content dictionary.
val contentMap: MXUsersDevicesMap<Any>,
// the transactionId
// the transactionId. If not provided, a transactionId will be created by the task
val transactionId: String? = null
)
}
@ -46,16 +46,23 @@ internal class DefaultSendToDeviceTask @Inject constructor(
messages = params.contentMap.map
)
// If params.transactionId is not provided, we create a unique txnId.
// It's important to do that outside the requestBlock parameter of executeRequest()
// to use the same value if the request is retried
val txnId = params.transactionId ?: createUniqueTxnId()
return executeRequest(
globalErrorReceiver,
canRetry = true,
maxRetriesCount = 3
) {
cryptoApi.sendToDevice(
params.eventType,
params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(),
sendToDeviceBody
eventType = params.eventType,
transactionId = txnId,
body = sendToDeviceBody
)
}
}
}
internal fun createUniqueTxnId() = UUID.randomUUID().toString()

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,11 +14,10 @@
* limitations under the License.
*/
package im.vector.app.features.call.conference
package org.matrix.android.sdk.internal.crypto.util
data class JitsiWidgetProperties(
val domain: String,
val confId: String?,
val displayName: String?,
val avatarUrl: String?
)
import java.util.UUID
internal object RequestIdHelper {
fun createUniqueRequestId() = UUID.randomUUID().toString()
}

View file

@ -17,6 +17,9 @@
package org.matrix.android.sdk.internal.database
import com.zhuinden.monarchy.Monarchy
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@ -24,20 +27,15 @@ import org.matrix.android.sdk.internal.database.model.EventInsertEntityFields
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import timber.log.Timber
import javax.inject.Inject
internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
private val eventDecryptor: EventDecryptor)
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>)
: RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventInsertEntity> {
it.where(EventInsertEntity::class.java)
override val query = Monarchy.Query {
it.where(EventInsertEntity::class.java).equalTo(EventInsertEntityFields.CAN_BE_PROCESSED, true)
}
override fun onChange(results: RealmResults<EventInsertEntity>) {
@ -86,23 +84,6 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
}
}
// private fun decryptIfNeeded(event: Event) {
// if (event.isEncrypted() && event.mxDecryptionResult == null) {
// try {
// val result = eventDecryptor.decryptEvent(event, event.roomId ?: "")
// event.mxDecryptionResult = OlmDecryptionResult(
// payload = result.clearEvent,
// senderKey = result.senderCurve25519Key,
// keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
// forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
// )
// } catch (e: MXCryptoError) {
// Timber.v("Failed to decrypt event")
// // TODO -> we should keep track of this and retry, or some processing will never be handled
// }
// }
// }
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
return processors.any {
it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType)

View file

@ -29,6 +29,7 @@ import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFie
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.EventInsertEntityFields
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
@ -51,7 +52,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
const val SESSION_STORE_SCHEMA_SC_VERSION = 2L
const val SESSION_STORE_SCHEMA_SC_VERSION_OFFSET = (1L shl 12)
const val SESSION_STORE_SCHEMA_VERSION = 16L +
const val SESSION_STORE_SCHEMA_VERSION = 17L +
SESSION_STORE_SCHEMA_SC_VERSION * SESSION_STORE_SCHEMA_SC_VERSION_OFFSET
@ -77,6 +78,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
if (oldVersion <= 13) migrateTo14(realm)
if (oldVersion <= 14) migrateTo15(realm)
if (oldVersion <= 15) migrateTo16(realm)
if (oldVersion <= 16) migrateTo17(realm)
if (oldScVersion <= 0) migrateToSc1(realm)
if (oldScVersion <= 1) migrateToSc2(realm)
@ -359,4 +361,10 @@ internal object RealmSessionStoreMigration : RealmMigration {
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
}
}
private fun migrateTo17(realm: DynamicRealm) {
Timber.d("Step 16 -> 17")
realm.schema.get("EventInsertEntity")
?.addField(EventInsertEntityFields.CAN_BE_PROCESSED, Boolean::class.java)
}
}

View file

@ -100,7 +100,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
avatarUrl = it.childSummaryEntity?.avatarUrl,
activeMemberCount = it.childSummaryEntity?.joinedMembersCount,
order = it.order,
autoJoin = it.autoJoin ?: false,
// autoJoin = it.autoJoin ?: false,
viaServers = it.viaServers.toList(),
parentRoomId = roomSummaryEntity.roomId,
suggested = it.suggested,

View file

@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.di.MoshiProvider
import io.realm.RealmObject
import io.realm.annotations.Index
import org.matrix.android.sdk.internal.extensions.assertIsManaged
internal open class EventEntity(@Index var eventId: String = "",
@Index var roomId: String = "",
@ -56,15 +57,22 @@ internal open class EventEntity(@Index var eventId: String = "",
companion object
fun setDecryptionResult(result: MXEventDecryptionResult) {
assertIsManaged()
val decryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java)
decryptionResultJson = adapter.toJson(decryptionResult)
decryptionErrorCode = null
decryptionErrorReason = null
// If we have an EventInsertEntity for the eventId we make sures it can be processed now.
realm.where(EventInsertEntity::class.java)
.equalTo(EventInsertEntityFields.EVENT_ID, eventId)
.findFirst()
?.canBeProcessed = true
}
}

View file

@ -23,7 +23,12 @@ import io.realm.RealmObject
* in EventEntity table.
*/
internal open class EventInsertEntity(var eventId: String = "",
var eventType: String = ""
var eventType: String = "",
/**
* This flag will be used to filter EventInsertEntity in EventInsertLiveObserver.
* Currently it's set to false when the event content is encrypted.
*/
var canBeProcessed: Boolean = true
) : RealmObject() {
private var insertTypeStr: String = EventInsertType.INCREMENTAL_SYNC.name

View file

@ -24,6 +24,7 @@ import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmQuery
import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.events.model.EventType
internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity {
val eventEntity = realm.where<EventEntity>()
@ -31,7 +32,8 @@ internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInse
.equalTo(EventEntityFields.ROOM_ID, roomId)
.findFirst()
return if (eventEntity == null) {
val insertEntity = EventInsertEntity(eventId = eventId, eventType = type).apply {
val canBeProcessed = type != EventType.ENCRYPTED || decryptionResultJson != null
val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = canBeProcessed).apply {
this.insertType = insertType
}
realm.insert(insertEntity)

View file

@ -30,6 +30,7 @@ internal object NetworkConstants {
// Identity server
const val URI_IDENTITY_PREFIX_PATH = "_matrix/identity/v2"
const val URI_IDENTITY_PATH_V2 = "$URI_IDENTITY_PREFIX_PATH/"
const val URI_IDENTITY_PATH_V1 = "_matrix/identity/api/v1/"
// Push Gateway
const val URI_PUSH_GATEWAY_PREFIX_PATH = "_matrix/push/v1/"

View file

@ -19,13 +19,13 @@
package org.matrix.android.sdk.internal.network
import com.squareup.moshi.JsonEncodingException
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ResponseBody
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.internal.di.MoshiProvider
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ResponseBody
import org.matrix.android.sdk.api.extensions.orFalse
import retrofit2.HttpException
import retrofit2.Response
import timber.log.Timber
@ -86,13 +86,18 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int, globalErrorReceiv
val matrixError = matrixErrorAdapter.fromJson(errorBodyStr)
if (matrixError != null) {
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
// Also send this error to the globalErrorReceiver, for a global management
globalErrorReceiver?.handleGlobalError(GlobalError.ConsentNotGivenError(matrixError.consentUri))
} else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& matrixError.code == MatrixError.M_UNKNOWN_TOKEN) {
// Also send this error to the globalErrorReceiver, for a global management
globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout.orFalse()))
// Also send following errors to the globalErrorReceiver, for a global management
when {
matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank() -> {
globalErrorReceiver?.handleGlobalError(GlobalError.ConsentNotGivenError(matrixError.consentUri))
}
httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& matrixError.code == MatrixError.M_UNKNOWN_TOKEN -> {
globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout.orFalse()))
}
matrixError.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT -> {
globalErrorReceiver?.handleGlobalError(GlobalError.ExpiredAccount)
}
}
return Failure.ServerError(matrixError, httpCode)

View file

@ -24,13 +24,21 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
internal fun RealmQuery<RoomSummaryEntity>.process(sortOrder: RoomSortOrder): RealmQuery<RoomSummaryEntity> {
when (sortOrder) {
RoomSortOrder.NAME -> {
RoomSortOrder.NAME -> {
sort(RoomSummaryEntityFields.DISPLAY_NAME, Sort.ASCENDING)
}
RoomSortOrder.ACTIVITY -> {
RoomSortOrder.ACTIVITY -> {
sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
}
RoomSortOrder.NONE -> {
RoomSortOrder.PRIORITY_AND_ACTIVITY -> {
sort(
arrayOf(
RoomSummaryEntityFields.IS_FAVOURITE,
RoomSummaryEntityFields.IS_LOW_PRIORITY,
RoomSummaryEntityFields.LAST_ACTIVITY_TIME),
arrayOf(Sort.DESCENDING, Sort.ASCENDING, Sort.DESCENDING))
}
RoomSortOrder.NONE -> {
}
}
return this

View file

@ -84,7 +84,7 @@ internal data class RoomVersions(
* }
* }
*/
@Json(name = "room_capabilities")
@Json(name = "org.matrix.msc3244.room_capabilities")
val roomCapabilities: JsonDict? = null
)

View file

@ -52,6 +52,7 @@ import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
import timber.log.Timber
import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection
@ -79,6 +80,7 @@ internal class DefaultIdentityService @Inject constructor(
private val identityApiProvider: IdentityApiProvider,
private val accountDataDataSource: UserAccountDataDataSource,
private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
private val sign3pidInvitationTask: DefaultSign3pidInvitationTask,
private val sessionParams: SessionParams
) : IdentityService, SessionLifecycleObserver {
@ -290,6 +292,14 @@ internal class DefaultIdentityService @Inject constructor(
return token.token
}
override suspend fun sign3pidInvitation(identiyServer: String, token: String, secret: String): SignInvitationResult {
return sign3pidInvitationTask.execute(Sign3pidInvitationTask.Params(
url = identiyServer,
token = token,
privateKey = secret
))
}
override fun addListener(listener: IdentityServiceListener) {
listeners.add(listener)
}

View file

@ -26,10 +26,12 @@ import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestOwn
import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForEmailBody
import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForMsisdnBody
import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenResponse
import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
/**
* Ref: https://matrix.org/docs/spec/identity_service/latest
@ -95,4 +97,16 @@ internal interface IdentityAPI {
@POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "validate/{medium}/submitToken")
suspend fun submitToken(@Path("medium") medium: String,
@Body body: IdentityRequestOwnershipParams): SuccessResult
/**
* https://matrix.org/docs/spec/identity_service/r0.3.0#post-matrix-identity-v2-sign-ed25519
*
* Have to rely on V1 for now
*/
@POST(NetworkConstants.URI_IDENTITY_PATH_V1 + "sign-ed25519")
suspend fun signInvitationDetails(
@Query("token") token: String,
@Query("private_key") privateKey: String,
@Query("mxid") mxid: String
): SignInvitationResult
}

View file

@ -0,0 +1,49 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.identity
import dagger.Lazy
import okhttp3.OkHttpClient
import org.matrix.android.sdk.internal.di.Unauthenticated
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.RetrofitFactory
import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface Sign3pidInvitationTask : Task<Sign3pidInvitationTask.Params, SignInvitationResult> {
data class Params(
val token: String,
val url: String,
val privateKey: String
)
}
internal class DefaultSign3pidInvitationTask @Inject constructor(
@Unauthenticated
private val okHttpClient: Lazy<OkHttpClient>,
private val retrofitFactory: RetrofitFactory,
@UserId private val userId: String
) : Sign3pidInvitationTask {
override suspend fun execute(params: Sign3pidInvitationTask.Params): SignInvitationResult {
val identityAPI = retrofitFactory
.create(okHttpClient, "https://${params.url}")
.create(IdentityAPI::class.java)
return identityAPI.signInvitationDetails(params.token, params.privateKey, userId)
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.identity.model
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class SignInvitationBody(
/**The Matrix user ID of the user accepting the invitation.*/
val mxid: String,
/**The token from the call to store- invite..*/
val token: String,
/** The private key, encoded as Unpadded base64. */
val private_key: String
)

View file

@ -0,0 +1,31 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.identity.model
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SignInvitationResult(
/** The Matrix user ID of the user accepting the invitation.*/
val mxid: String,
/** The Matrix user ID of the user who sent the invitation.*/
val sender: String,
/**The token from the call to store- invite..*/
val signatures: Map<String, *>,
/** The token for the invitation */
val token: String
)

View file

@ -15,6 +15,8 @@
*/
package org.matrix.android.sdk.internal.session.notification
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.pushrules.Action
import org.matrix.android.sdk.api.pushrules.PushRuleService
@ -26,6 +28,7 @@ import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.database.mapper.PushRulesMapper
import org.matrix.android.sdk.internal.database.model.PushRuleEntity
import org.matrix.android.sdk.internal.database.model.PushRulesEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
@ -117,8 +120,8 @@ internal class DefaultPushRuleService @Inject constructor(
updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, ruleId, enable, actions))
}
override suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) {
removePushRuleTask.execute(RemovePushRuleTask.Params(kind, pushRule))
override suspend fun removePushRule(kind: RuleKind, ruleId: String) {
removePushRuleTask.execute(RemovePushRuleTask.Params(kind, ruleId))
}
override fun removePushRuleListener(listener: PushRuleService.PushRuleListener) {
@ -211,4 +214,19 @@ internal class DefaultPushRuleService @Inject constructor(
}
}
}
override fun getKeywords(): LiveData<Set<String>> {
// Keywords are all content rules that don't start with '.'
val liveData = monarchy.findAllMappedWithChanges(
{ realm ->
PushRulesEntity.where(realm, RuleScope.GLOBAL, RuleSetKey.CONTENT)
},
{ result ->
result.pushRules.map(PushRuleEntity::ruleId).filter { !it.startsWith(".") }
}
)
return Transformations.map(liveData) { results ->
results.firstOrNull().orEmpty().toSet()
}
}
}

View file

@ -78,6 +78,10 @@ internal class ViaParameterFinder @Inject constructor(
.toSet()
}
// not used much for now but as per MSC1772
// the via parameter of m.space.child must contain a via key which gives a list of candidate servers that can be used to join the room.
// It is possible for the list of candidate servers and the list of authorised servers to diverge.
// It may not be possible for a user to join a room if there's no overlap between these
fun computeViaParamsForRestricted(roomId: String, max: Int): List<String> {
val userThatCanInvite = roomGetterProvider.get().getRoom(roomId)
?.getRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.JOIN) })

View file

@ -0,0 +1,25 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.pushers
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class EnabledBody(
@Json(name = "enabled")
val enabled: Boolean
)

View file

@ -19,7 +19,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal class GetPushersResponse(
internal data class GetPushersResponse(
@Json(name = "pushers")
val pushers: List<JsonPusher>? = null
)

View file

@ -41,7 +41,7 @@ internal interface PushRulesApi {
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "pushrules/global/{kind}/{ruleId}/enabled")
suspend fun updateEnableRuleStatus(@Path("kind") kind: String,
@Path("ruleId") ruleId: String,
@Body enable: Boolean?)
@Body enabledBody: EnabledBody)
/**
* Update the ruleID action

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.session.pushers
import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
@ -25,7 +24,7 @@ import javax.inject.Inject
internal interface RemovePushRuleTask : Task<RemovePushRuleTask.Params, Unit> {
data class Params(
val kind: RuleKind,
val pushRule: PushRule
val ruleId: String
)
}
@ -36,7 +35,7 @@ internal class DefaultRemovePushRuleTask @Inject constructor(
override suspend fun execute(params: RemovePushRuleTask.Params) {
return executeRequest(globalErrorReceiver) {
pushRulesApi.deleteRule(params.kind.value, params.pushRule.ruleId)
pushRulesApi.deleteRule(params.kind.value, params.ruleId)
}
}
}

View file

@ -39,7 +39,11 @@ internal class DefaultUpdatePushRuleActionsTask @Inject constructor(
override suspend fun execute(params: UpdatePushRuleActionsTask.Params) {
executeRequest(globalErrorReceiver) {
pushRulesApi.updateEnableRuleStatus(params.kind.value, params.ruleId, enable = params.enable)
pushRulesApi.updateEnableRuleStatus(
params.kind.value,
params.ruleId,
EnabledBody(params.enable)
)
}
if (params.actions != null) {
val body = mapOf("actions" to params.actions.toJson())

View file

@ -35,7 +35,11 @@ internal class DefaultUpdatePushRuleEnableStatusTask @Inject constructor(
override suspend fun execute(params: UpdatePushRuleEnableStatusTask.Params) {
return executeRequest(globalErrorReceiver) {
pushRulesApi.updateEnableRuleStatus(params.kind.value, params.pushRule.ruleId, params.enabled)
pushRulesApi.updateEnableRuleStatus(
params.kind.value,
params.pushRule.ruleId,
EnabledBody(params.enabled)
)
}
}
}

View file

@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
@ -122,6 +123,12 @@ internal class DefaultRoomService @Inject constructor(
joinRoomTask.execute(JoinRoomTask.Params(roomIdOrAlias, reason, viaServers))
}
override suspend fun joinRoom(roomId: String,
reason: String?,
thirdPartySigned: SignInvitationResult) {
joinRoomTask.execute(JoinRoomTask.Params(roomId, reason, thirdPartySigned = thirdPartySigned))
}
override suspend fun markAllAsRead(roomIds: List<String>) {
markAllRoomsReadTask.execute(MarkAllRoomsReadTask.Params(roomIds))
}

View file

@ -159,7 +159,8 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/receipt/{receiptType}/{eventId}")
suspend fun sendReceipt(@Path("roomId") roomId: String,
@Path("receiptType") receiptType: String,
@Path("eventId") eventId: String)
@Path("eventId") eventId: String,
@Body body: JsonDict = emptyMap())
/**
* Invite a user to the given room.
@ -253,7 +254,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "join/{roomIdOrAlias}")
suspend fun join(@Path("roomIdOrAlias") roomIdOrAlias: String,
@Query("server_name") viaServers: List<String>,
@Body params: Map<String, String?>): JoinRoomResponse
@Body params: JsonDict): JoinRoomResponse
/**
* Leave the given room.

View file

@ -28,6 +28,8 @@ import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.internal.session.DefaultFileService
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.directory.DirectoryAPI
import org.matrix.android.sdk.internal.session.identity.DefaultSign3pidInvitationTask
import org.matrix.android.sdk.internal.session.identity.Sign3pidInvitationTask
import org.matrix.android.sdk.internal.session.room.accountdata.DefaultUpdateRoomAccountDataTask
import org.matrix.android.sdk.internal.session.room.accountdata.UpdateRoomAccountDataTask
import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask
@ -253,4 +255,7 @@ internal abstract class RoomModule {
@Binds
abstract fun bindRoomVersionUpgradeTask(task: DefaultRoomVersionUpgradeTask): RoomVersionUpgradeTask
@Binds
abstract fun bindSign3pidInvitationTask(task: DefaultSign3pidInvitationTask): Sign3pidInvitationTask
}

View file

@ -123,7 +123,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
this.isDirect = true
}
}
val directChats = directChatsHelper.getLocalUserAccount()
val directChats = directChatsHelper.getLocalDirectMessages()
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats))
}

View file

@ -20,22 +20,30 @@ import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
import org.matrix.android.sdk.internal.session.user.UserEntityFactory
import javax.inject.Inject
internal class RoomMemberEventHandler @Inject constructor() {
internal class RoomMemberEventHandler @Inject constructor(
@UserId private val myUserId: String
) {
fun handle(realm: Realm, roomId: String, event: Event): Boolean {
fun handle(realm: Realm, roomId: String, event: Event, aggregator: SyncResponsePostTreatmentAggregator? = null): Boolean {
if (event.type != EventType.STATE_ROOM_MEMBER) {
return false
}
val userId = event.stateKey ?: return false
val roomMember = event.getFixedRoomMemberContent()
return handle(realm, roomId, userId, roomMember)
return handle(realm, roomId, userId, roomMember, aggregator)
}
fun handle(realm: Realm, roomId: String, userId: String, roomMember: RoomMemberContent?): Boolean {
fun handle(realm: Realm,
roomId: String,
userId: String,
roomMember: RoomMemberContent?,
aggregator: SyncResponsePostTreatmentAggregator? = null): Boolean {
if (roomMember == null) {
return false
}
@ -45,6 +53,14 @@ internal class RoomMemberEventHandler @Inject constructor() {
val userEntity = UserEntityFactory.create(userId, roomMember)
realm.insertOrUpdate(userEntity)
}
// check whether this new room member event may be used to update the directs dictionary in account data
// this is required to handle correctly invite by email in DM
val mxId = roomMember.thirdPartyInvite?.signed?.mxid
if (mxId != null && mxId != myUserId) {
aggregator?.directChatsToCheck?.put(roomId, mxId)
}
return true
}
}

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.membership.joining
import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
@ -29,6 +30,7 @@ import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask
@ -40,7 +42,8 @@ internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
data class Params(
val roomIdOrAlias: String,
val reason: String?,
val viaServers: List<String> = emptyList()
val viaServers: List<String> = emptyList(),
val thirdPartySigned : SignInvitationResult? = null
)
}
@ -59,12 +62,16 @@ internal class DefaultJoinRoomTask @Inject constructor(
return
}
roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining)
val extraParams = mutableMapOf<String, Any>().apply {
params.reason?.let { this["reason"] = it }
params.thirdPartySigned?.let { this["third_party_signed"] = it.toContent() }
}
val joinRoomResponse = try {
executeRequest(globalErrorReceiver) {
roomAPI.join(
roomIdOrAlias = params.roomIdOrAlias,
viaServers = params.viaServers.take(3),
params = mapOf("reason" to params.reason)
params = extraParams
)
}
} catch (failure: Throwable) {

View file

@ -45,7 +45,7 @@ internal class DefaultSetRoomNotificationStateTask @Inject constructor(@SessionD
PushRuleEntity.where(it, scope = RuleScope.GLOBAL, ruleId = params.roomId).findFirst()?.toRoomPushRule()
}
if (currentRoomPushRule != null) {
removePushRuleTask.execute(RemovePushRuleTask.Params(currentRoomPushRule.kind, currentRoomPushRule.rule))
removePushRuleTask.execute(RemovePushRuleTask.Params(currentRoomPushRule.kind, currentRoomPushRule.rule.ruleId))
}
val newRoomPushRule = params.roomNotificationState.toRoomPushRule(params.roomId)
if (newRoomPushRule != null) {

View file

@ -43,7 +43,7 @@ internal class RoomChildRelationInfo(
data class SpaceChildInfo(
val roomId: String,
val order: String?,
val autoJoin: Boolean,
// val autoJoin: Boolean,
val viaServers: List<String>
)
@ -71,7 +71,7 @@ internal class RoomChildRelationInfo(
SpaceChildInfo(
roomId = it.stateKey,
order = scc.validOrder(),
autoJoin = scc.autoJoin ?: false,
// autoJoin = scc.autoJoin ?: false,
viaServers = via
)
}

View file

@ -185,7 +185,7 @@ internal class DefaultSendService @AssistedInject constructor(
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.AUDIO,
waveform = messageContent.audioWaveformInfo?.waveform
waveform = messageContent.audioWaveformInfo?.waveform?.filterNotNull()
)
localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)

View file

@ -77,7 +77,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
val timelineEvent = timelineEventMapper.map(timelineEventEntity)
timelineInput.onLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent)
taskExecutor.executorScope.asyncTransaction(monarchy) { realm ->
val eventInsertEntity = EventInsertEntity(event.eventId, event.type).apply {
val eventInsertEntity = EventInsertEntity(event.eventId, event.type, canBeProcessed = true).apply {
this.insertType = EventInsertType.LOCAL_ECHO
}
realm.insert(eventInsertEntity)

View file

@ -182,7 +182,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
override suspend fun setJoinRuleRestricted(allowList: List<String>) {
// we need to compute correct via parameters and check if PL are correct
val allowEntries = allowList.map { spaceId ->
RoomJoinRulesAllowEntry(spaceId, viaParameterFinder.computeViaParamsForRestricted(spaceId, 3))
RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
}
updateJoinRule(RoomJoinRules.RESTRICTED, null, allowEntries)
}

View file

@ -234,7 +234,7 @@ internal class RoomSummaryUpdater @Inject constructor(
this.childRoomId = child.roomId
this.childSummaryEntity = RoomSummaryEntity.where(realm, child.roomId).findFirst()
this.order = child.order
this.autoJoin = child.autoJoin
// this.autoJoin = child.autoJoin
this.viaServers.addAll(child.viaServers)
}
)

View file

@ -106,7 +106,8 @@ internal class TimelineEventDecryptor @Inject constructor(
val result = cryptoService.decryptEvent(request.event, timelineId)
Timber.v("Successfully decrypted event ${event.eventId}")
realm.executeTransaction {
EventEntity.where(it, eventId = event.eventId ?: "")
val eventId = event.eventId ?: ""
EventEntity.where(it, eventId = eventId)
.findFirst()
?.setDecryptionResult(result)
}

View file

@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
@ -50,16 +51,24 @@ internal class DefaultSpace(
override suspend fun addChildren(roomId: String,
viaServers: List<String>?,
order: String?,
autoJoin: Boolean,
// autoJoin: Boolean,
suggested: Boolean?) {
// Find best via
val bestVia = viaServers
?: (spaceSummaryDataSource.getRoomSummary(roomId)
?.takeIf { it.joinRules == RoomJoinRules.RESTRICTED }
?.let {
// for restricted room, best to take via from users that can invite in the
// child room
viaParameterFinder.computeViaParamsForRestricted(roomId, 3)
}
?: viaParameterFinder.computeViaParams(roomId, 3))
room.sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD,
stateKey = roomId,
body = SpaceChildContent(
via = viaServers ?: viaParameterFinder.computeViaParams(roomId, 3),
autoJoin = autoJoin,
via = bestVia,
order = order,
suggested = suggested
).toContent()
@ -80,7 +89,7 @@ internal class DefaultSpace(
body = SpaceChildContent(
order = null,
via = null,
autoJoin = null,
// autoJoin = null,
suggested = null
).toContent()
)
@ -105,35 +114,35 @@ internal class DefaultSpace(
body = SpaceChildContent(
order = order,
via = existing.via,
autoJoin = existing.autoJoin,
// autoJoin = existing.autoJoin,
suggested = existing.suggested
).toContent()
)
}
override suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) {
val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
.firstOrNull()
?.content.toModel<SpaceChildContent>()
?: throw IllegalArgumentException("$roomId is not a child of this space")
if (existing.autoJoin == autoJoin) {
// nothing to do?
return
}
// edit state event and set via to null
room.sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD,
stateKey = roomId,
body = SpaceChildContent(
order = existing.order,
via = existing.via,
autoJoin = autoJoin,
suggested = existing.suggested
).toContent()
)
}
// override suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) {
// val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
// .firstOrNull()
// ?.content.toModel<SpaceChildContent>()
// ?: throw IllegalArgumentException("$roomId is not a child of this space")
//
// if (existing.autoJoin == autoJoin) {
// // nothing to do?
// return
// }
//
// // edit state event and set via to null
// room.sendStateEvent(
// eventType = EventType.STATE_SPACE_CHILD,
// stateKey = roomId,
// body = SpaceChildContent(
// order = existing.order,
// via = existing.via,
// autoJoin = autoJoin,
// suggested = existing.suggested
// ).toContent()
// )
// }
override suspend fun setChildrenSuggested(roomId: String, suggested: Boolean) {
val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
@ -152,7 +161,7 @@ internal class DefaultSpace(
body = SpaceChildContent(
order = existing.order,
via = existing.via,
autoJoin = existing.autoJoin,
// autoJoin = existing.autoJoin,
suggested = suggested
).toContent()
)

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.space
import android.net.Uri
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
@ -27,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
@ -34,6 +36,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
import org.matrix.android.sdk.api.session.space.JoinSpaceResult
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.session.space.SpaceHierarchyData
import org.matrix.android.sdk.api.session.space.SpaceService
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
@ -108,53 +111,65 @@ internal class DefaultSpaceService @Inject constructor(
override suspend fun querySpaceChildren(spaceId: String,
suggestedOnly: Boolean?,
autoJoinedOnly: Boolean?): Pair<RoomSummary, List<SpaceChildInfo>> {
return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId, suggestedOnly, autoJoinedOnly)).let { response ->
limit: Int?,
from: String?,
knownStateList: List<Event>?): SpaceHierarchyData {
return resolveSpaceInfoTask.execute(
ResolveSpaceInfoTask.Params(
spaceId = spaceId, limit = limit, maxDepth = 1, from = from, suggestedOnly = suggestedOnly
)
).let { response ->
val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId }
Pair(
first = RoomSummary(
roomId = spaceDesc?.roomId ?: spaceId,
roomType = spaceDesc?.roomType,
name = spaceDesc?.name ?: "",
displayName = spaceDesc?.name ?: "",
topic = spaceDesc?.topic ?: "",
joinedMembersCount = spaceDesc?.numJoinedMembers,
avatarUrl = spaceDesc?.avatarUrl ?: "",
encryptionEventTs = null,
typingUsers = emptyList(),
isEncrypted = false,
flattenParentIds = emptyList()
),
second = response.rooms
?.filter { it.roomId != spaceId }
?.flatMap { childSummary ->
response.events
?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
?.mapNotNull { childStateEv ->
// create a child entry for everytime this room is the child of a space
// beware that a room could appear then twice in this list
childStateEv.content.toModel<SpaceChildContent>()?.let { childStateEvContent ->
SpaceChildInfo(
childRoomId = childSummary.roomId,
isKnown = true,
roomType = childSummary.roomType,
name = childSummary.name,
topic = childSummary.topic,
avatarUrl = childSummary.avatarUrl,
order = childStateEvContent.order,
autoJoin = childStateEvContent.autoJoin ?: false,
viaServers = childStateEvContent.via.orEmpty(),
activeMemberCount = childSummary.numJoinedMembers,
parentRoomId = childStateEv.roomId,
suggested = childStateEvContent.suggested,
canonicalAlias = childSummary.canonicalAlias,
aliases = childSummary.aliases,
worldReadable = childSummary.worldReadable
)
}
}.orEmpty()
}
.orEmpty()
val root = RoomSummary(
roomId = spaceDesc?.roomId ?: spaceId,
roomType = spaceDesc?.roomType,
name = spaceDesc?.name ?: "",
displayName = spaceDesc?.name ?: "",
topic = spaceDesc?.topic ?: "",
joinedMembersCount = spaceDesc?.numJoinedMembers,
avatarUrl = spaceDesc?.avatarUrl ?: "",
encryptionEventTs = null,
typingUsers = emptyList(),
isEncrypted = false,
flattenParentIds = emptyList(),
canonicalAlias = spaceDesc?.canonicalAlias,
joinRules = RoomJoinRules.PUBLIC.takeIf { spaceDesc?.worldReadable == true }
)
val children = response.rooms
?.filter { it.roomId != spaceId }
?.flatMap { childSummary ->
(spaceDesc?.childrenState ?: knownStateList)
?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
?.mapNotNull { childStateEv ->
// create a child entry for everytime this room is the child of a space
// beware that a room could appear then twice in this list
childStateEv.content.toModel<SpaceChildContent>()?.let { childStateEvContent ->
SpaceChildInfo(
childRoomId = childSummary.roomId,
isKnown = true,
roomType = childSummary.roomType,
name = childSummary.name,
topic = childSummary.topic,
avatarUrl = childSummary.avatarUrl,
order = childStateEvContent.order,
// autoJoin = childStateEvContent.autoJoin ?: false,
viaServers = childStateEvContent.via.orEmpty(),
activeMemberCount = childSummary.numJoinedMembers,
parentRoomId = childStateEv.roomId,
suggested = childStateEvContent.suggested,
canonicalAlias = childSummary.canonicalAlias,
aliases = childSummary.aliases,
worldReadable = childSummary.worldReadable
)
}
}.orEmpty()
}
.orEmpty()
SpaceHierarchyData(
rootSummary = root,
children = children,
childrenState = spaceDesc?.childrenState.orEmpty(),
nextToken = response.nextBatch
)
}
}

View file

@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.space
import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.space.JoinSpaceResult
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
@ -84,39 +83,39 @@ internal class DefaultJoinSpaceTask @Inject constructor(
// after that i should have the children (? do I need to paginate to get state)
val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias)
Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.spaceChildren?.size}")
summary?.spaceChildren?.forEach {
// summary?.spaceChildren?.forEach {
// val childRoomSummary = it.roomSummary ?: return@forEach
Timber.v("## Space: Processing child :[${it.childRoomId}] autoJoin:${it.autoJoin}")
if (it.autoJoin) {
// I should try to join as well
if (it.roomType == RoomType.SPACE) {
// recursively join auto-joined child of this space?
when (val subspaceJoinResult = execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) {
JoinSpaceResult.Success -> {
// nop
}
is JoinSpaceResult.Fail -> {
errors[it.childRoomId] = subspaceJoinResult.error
}
is JoinSpaceResult.PartialSuccess -> {
errors.putAll(subspaceJoinResult.failedRooms)
}
}
} else {
try {
Timber.v("## Space: Joining room child ${it.childRoomId}")
joinRoomTask.execute(JoinRoomTask.Params(
roomIdOrAlias = it.childRoomId,
reason = "Auto-join parent space",
viaServers = it.viaServers
))
} catch (failure: Throwable) {
errors[it.childRoomId] = failure
Timber.e("## Space: Failed to join room child ${it.childRoomId}")
}
}
}
}
// Timber.v("## Space: Processing child :[${it.childRoomId}] suggested:${it.suggested}")
// if (it.autoJoin) {
// // I should try to join as well
// if (it.roomType == RoomType.SPACE) {
// // recursively join auto-joined child of this space?
// when (val subspaceJoinResult = execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) {
// JoinSpaceResult.Success -> {
// // nop
// }
// is JoinSpaceResult.Fail -> {
// errors[it.childRoomId] = subspaceJoinResult.error
// }
// is JoinSpaceResult.PartialSuccess -> {
// errors.putAll(subspaceJoinResult.failedRooms)
// }
// }
// } else {
// try {
// Timber.v("## Space: Joining room child ${it.childRoomId}")
// joinRoomTask.execute(JoinRoomTask.Params(
// roomIdOrAlias = it.childRoomId,
// reason = "Auto-join parent space",
// viaServers = it.viaServers
// ))
// } catch (failure: Throwable) {
// errors[it.childRoomId] = failure
// Timber.e("## Space: Failed to join room child ${it.childRoomId}")
// }
// }
// }
// }
return if (errors.isEmpty()) {
JoinSpaceResult.Success

View file

@ -24,24 +24,12 @@ import javax.inject.Inject
internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, SpacesResponse> {
data class Params(
val spaceId: String,
val maxRoomPerSpace: Int?,
val limit: Int,
val batchToken: String?,
val suggestedOnly: Boolean?,
val autoJoinOnly: Boolean?
) {
companion object {
fun withId(spaceId: String, suggestedOnly: Boolean?, autoJoinOnly: Boolean?) =
Params(
spaceId = spaceId,
maxRoomPerSpace = 10,
limit = 20,
batchToken = null,
suggestedOnly = suggestedOnly,
autoJoinOnly = autoJoinOnly
)
}
}
val limit: Int?,
val maxDepth: Int?,
val from: String?,
val suggestedOnly: Boolean?
// val autoJoinOnly: Boolean?
)
}
internal class DefaultResolveSpaceInfoTask @Inject constructor(
@ -49,15 +37,13 @@ internal class DefaultResolveSpaceInfoTask @Inject constructor(
private val globalErrorReceiver: GlobalErrorReceiver
) : ResolveSpaceInfoTask {
override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse {
val body = SpaceSummaryParams(
maxRoomPerSpace = params.maxRoomPerSpace,
limit = params.limit,
batch = params.batchToken ?: "",
autoJoinedOnly = params.autoJoinOnly,
suggestedOnly = params.suggestedOnly
)
return executeRequest(globalErrorReceiver) {
spaceApi.getSpaces(params.spaceId, body)
spaceApi.getSpaceHierarchy(
spaceId = params.spaceId,
suggestedOnly = params.suggestedOnly,
limit = params.limit,
maxDepth = params.maxDepth,
from = params.from)
}
}
}

Some files were not shown because too many files have changed in this diff Show more